Skip to content

AHABHGK

Vue3 Compat

SourceCode, Front End Framework3 min read

Table of Contents

Vue3 中内置组件和一些其他新特性的实现原理,作为上一篇的补充

Fragment

runtime-core/components/fragment.js
1export const Fragment = {
2 patch(
3 { mountChildren, patchChildren, renderOptions },
4 { n1, n2, container, isSVG, anchor }
5 ) {
6 if (n1 == null) {
7 const {
8 createText: hostCreateText,
9 insert: hostInsert,
10 } = renderOptions
11 const fragmentStartAnchor = n2.node = hostCreateText('')
12 const fragmentEndAnchor = n2.anchor = hostCreateText('')
13 hostInsert(fragmentStartAnchor, container, anchor)
14 hostInsert(fragmentEndAnchor, container, anchor)
15 mountChildren(n2, container, isSVG, fragmentEndAnchor)
16 } else {
17 n2.node = n1.node
18 n2.anchor = n1.anchor
19 patchChildren(n1, n2, container, isSVG)
20 }
21 },
22
23 getNode(internals, { vnode }) { // 插入到它的前面,需要从头部拿
24 return vnode.node
25 },
26
27 getNextSibling({ renderOptions }, { vnode }) { // nextSibling 需要从尾部拿
28 return renderOptions.nextSibling(vnode.anchor)
29 },
30
31 move({ move, renderOptions }, { vnode, container, anchor }) {
32 const { insert: hostInsert } = renderOptions
33 const fragmentStartAnchor = vnode.node
34 const fragmentEndAnchor = vnode.anchor
35 hostInsert(fragmentStartAnchor, container, anchor)
36 for (let child of vnode.children) {
37 move(child, container, anchor)
38 }
39 hostInsert(fragmentEndAnchor, container, anchor)
40 },
41
42 unmount({ unmount, renderOptions }, { vnode, doRemove }) {
43 const { remove: hostRemove } = renderOptions
44 hostRemove(vnode.node)
45 vnode.children.forEach(c => unmount(c, doRemove))
46 hostRemove(vnode.anchor)
47 },
48}

这五个方法会在哪里调用可以看上一篇,有具体的讲解和代码,Fragment 就是直接将子节点进行渲染,本身可以用两个 placeholder 来标记头部和尾部,因为 Fragment 的 nextSibling 是尾部 placehoder 的 nextSibling,而 getNode 用于插入到 Fragment 前面,所以返回的是 Fragment 的头部 placeholder

Teleport

Teleport 很像 Fragment,唯一的不同就是 Teleport 把子节点渲染到 target 节点上

runtime-core/components/teleport.js
1export const Teleport = {
2 patch(
3 { renderOptions, mountChildren, patchChildren, move },
4 { n1, n2, container, isSVG, anchor },
5 ) {
6 if (n1 == null) {
7 const teleportStartAnchor = n2.node = renderOptions.createText('')
8 const teleportEndAnchor = n2.anchor = renderOptions.createText('')
9 renderOptions.insert(teleportStartAnchor, container, anchor)
10 renderOptions.insert(teleportEndAnchor, container, anchor)
11 const target = renderOptions.querySelector(n2.props.to)
12 n2.target = target
13 mountChildren(n2, target, isSVG, null)
14 } else {
15 n2.node = n1.node
16 n2.anchor = n1.anchor
17 n2.target = n1.target
18 patchChildren(n1, n2, n2.target, isSVG)
19
20 if (n1.props.to !== n2.props.to) {
21 const target = renderOptions.querySelector(n2.props.to)
22 n2.target = target
23 for (let child of n2.children) {
24 move(child, container, null)
25 }
26 }
27 }
28 },
29
30 getNode(internals, { vnode }) {
31 return vnode.node
32 },
33
34 getNextSibling({ renderOptions }, { vnode }) {
35 return renderOptions.nextSibling(vnode.anchor)
36 },
37
38 move({ renderOptions, move }, { vnode, container, anchor }) {
39 const { insert: hostInsert } = renderOptions
40 const teleportStartAnchor = vnode.node
41 const teleportEndAnchor = vnode.anchor
42 hostInsert(teleportStartAnchor, container, anchor)
43 hostInsert(teleportEndAnchor, container, anchor)
44 },
45
46 unmount({ renderOptions, unmount }, { vnode }) {
47 const { remove: hostRemove } = renderOptions
48 hostRemove(vnode.node)
49 vnode.children.forEach(c => unmount(c))
50 hostRemove(vnode.anchor)
51 },
52}

不同于 ReactDOM.createProtal 由于 ReactDOM 有一个事件的合成层,可以在这里做一些 hack,使 Portal 的父组件可以捕捉到 Portal 中的事件,Vue3、Preact 由于没有实现事件合成层,所以父组件不能捕捉到 Teleport 中的事件,但相应的减少了很多的代码量,包的体积减小很多

Inject / Provide

直接看实现

runtime-core/inject.js
1import { isFunction } from '../shared'
2import { getCurrentInstance } from './component'
3
4export const provide = (key, value) => {
5 const currentInstance = getCurrentInstance()
6 if (!currentInstance) {
7 console.warn(`provide() can only be used inside setup().`)
8 } else {
9 let { provides } = currentInstance
10 const parentProvides = currentInstance.parent && currentInstance.parent.provides
11 if (parentProvides === provides) {
12 provides = currentInstance.provides = Object.create(parentProvides)
13 }
14 provides[key] = value
15 }
16}
17
18export const inject = (key, defaultValue) => {
19 const currentInstance = getCurrentInstance()
20 if (currentInstance) {
21 const { provides } = currentInstance
22 if (key in provides) {
23 return provides[key]
24 } else if (arguments.length > 1) { // defaultValue 可以传入 undefined
25 return isFunction(defaultValue)
26 ? defaultValue()
27 : defaultValue
28 } else {
29 console.warn(`injection "${String(key)}" not found.`)
30 }
31 } else {
32 console.warn(`inject() can only be used inside setup() or functional components.`)
33 }
34}

可以看出来 provides 是放在 instance 上的,每个 instance 的 provides 都是通过 Object.create 继承 parentInstance 的 provides

provide 调用时就是拿到 currentInstance,然后继承 currentInstance.parent 的 provides,再像上面通过 key 添加属性;inject 就是拿到 currentInstance 的 provides,再通过 key 取值即可,比较巧妙的就是 defaultValue 对于 undefined 的处理

之前我们的 runtime 并没有再 instance 上放 provides 属性,而且怎样去拿 parentInstance,接下来我们修改之前写的 runtime

runtime-core/renderer.js
1const processComponent = (n1, n2, container, isSVG, anchor) => {
2 if (n1 == null) {
3 const instance = n2.instance = {
4 // ...
5 parent: null,
6 provides: null,
7 }
8 const parentInstance = instance.parent = getParentInstance(n2)
9 instance.provides = parentInstance ? parentInstance.provides : Object.create(null) // 没有 parentInstance 说明是根组件,它的 provides 我们初始化成空对象
10 } // ...
11}
runtime/component.js
1export const getParentInstance = (vnode) => {
2 let parentVNode = vnode.parent
3 while (parentVNode != null) {
4 if (parentVNode.instance != null) return parentVNode.instance
5 parentVNode = parentVNode.parent
6 }
7 return null
8}

源码中 parentInstance 是类似于 anchor 作为一个参数一层一层传下来的,之后的 parentSuspense 也是,我们这里尽量简化,通过判断 .parent 链中是否有 instance 进行查找

onErrorCaptured

我们来实现我们唯一没有砍掉的钩子……

runtime-core/error-handling.js
1import { getCurrentInstance } from './component'
2
3export const onErrorCaptured = (errorHandler) => {
4 const instance = getCurrentInstance()
5 if (instance.errorCapturedHooks == null) { // 这样不用修改 renderer 中的代码了
6 instance.errorCapturedHooks = []
7 }
8 instance.errorCapturedHooks.push(errorHandler)
9}
10
11export const callWithErrorHandling = (fn, instance, args = []) => {
12 let res
13 try {
14 res = fn(...args)
15 } catch (e) {
16 handleError(e, instance)
17 }
18 return res
19}
20
21export const handleError = (error, instance) => {
22 if (instance) {
23 let cur = instance.parent
24 while (cur) {
25 const errorCapturedHooks = cur.errorCapturedHooks
26 if (errorCapturedHooks) {
27 for (let errorHandler of errorCapturedHooks) {
28 if (errorHandler(error)) {
29 return
30 }
31 }
32 }
33 cur = cur.parent
34 }
35 }
36 console.warn('Unhandled error', error)
37}

onErrorCaptured 就是添加错误处理的函数,通过 handleError 来从 instance.parent 中调用这些函数,知道返回 true 为止,而 callWithErrorHandling 是用来触发 handleError 的,我们对于用户可能出错的地方(可能有副作用的地方)调用时包裹一层 callWithErrorHandling 即可

runtime-core/api-watch.js
1export const watchEffect = (cb, { onTrack, onTrigger } = {}) => {
2 let cleanup
3 const onInvalidate = (fn) => {
4 cleanup = e.options.onStop = () => callWithErrorHandling(fn, instance)
5 }
6 const getter = () => {
7 if (cleanup) {
8 cleanup()
9 }
10 return callWithErrorHandling(cb, instance, [onInvalidate])
11 }
12 // ...
13}

Suspense

Vue3 在更新时遇到 Suspense 是在内存中创建一个 hiddenContianer,在内存中继续渲染 children,渲染 children 时如果遇到 async setup 会隐式的返回一个 Promise,Suspense 通过 register 接收这个 Promise,渲染完 children 后判断是否有接收 Promise,如果没有则把 hiddenContainer 中的 children 移动到 container 中,有则渲染 fallback 作为子节点,之后所有接收到的 Promise 在 resolve 之后再把 hiddenContainer 中的 children 移动到 container 中

在内存中创建 hiddenContainer 去渲染 children 是因为 Suspense 必须要根据是否有接收到 Promise 判断渲染 fallback 还是 children,而 Promise 只来自执行 children 中的 async setup

Suspense 的处理主要分为两部分,一部分是 Suspense 本身的处理,另一部分是对 async setup 子组件的处理,首先来看 Suspense 本身

runtime-core/components/suspense.js
1const createSuspense = (vnode, container, isSVG, anchor, internals, hiddenContainer) => {
2 const suspense = {
3 deps: [],
4 container,
5 anchor,
6 isSVG,
7 hiddenContainer,
8 resolve() {
9 internals.unmount(vnode.props.fallback)
10 internals.move(vnode.props.children, suspense.container, suspense.anchor)
11 vnode.node = internals.getNode(vnode.props.children)
12 },
13 register(instance, setupRenderEffect) {
14 // ...
15 },
16 }
17 return suspense
18}
19
20export const Suspense = {
21 patch(
22 internals,
23 { n1, n2, container, isSVG, anchor },
24 ) {
25 if (n1 == null) {
26 const hiddenContainer = internals.renderOptions.createElement('div')
27 const suspense = n2.suspense = createSuspense(n2, container, isSVG, anchor, internals, hiddenContainer)
28 internals.mountChildren(n2, hiddenContainer, isSVG, null)
29 internals.patch(null, n2.props.fallback, container, isSVG, anchor)
30 n2.node = internals.getNode(n2.props.fallback)
31 if (suspense.deps.length === 0) {
32 suspense.resolve()
33 }
34 } else {
35 // patchSuspense
36 }
37 },
38
39 getNode(internals, { vnode }) {
40 return vnode.node
41 },
42
43 getNextSibling({ renderOptions }, { vnode }) {
44 return renderOptions.nextSibling(vnode.node)
45 },
46
47 move({ move }, { vnode, container, anchor }) {
48 if (vnode.suspense.deps.length) {
49 move(vnode.props.fallback, container, anchor)
50 } else {
51 move(vnode.props.children, container, anchor)
52 }
53 vnode.suspense.container = container
54 vnode.suspense.anchor = anchor
55 },
56
57 unmount({ unmount }, { vnode, doRemove }) {
58 if (vnode.suspense.deps.length) {
59 unmount(vnode.props.fallback, doRemove)
60 } else {
61 unmount(vnode.props.children, doRemove)
62 }
63 },
64}
65
66export const getParentSuspense = (vnode) => {
67 vnode = vnode.parent
68 while (vnode) {
69 if (vnode.type === Suspense) return vnode.suspense
70 vnode = vnode.parent
71 }
72 return null
73}

我们实现的很简陋,可以看到核心逻辑就是创建一个 hiddenContainer,在这里面渲染 children,然后 createSuspense 创建实例,resolve 的时候就是把 fallback unmount 掉再把 hiddContainer 中的移动到 container 中,move 的时候 container 和 anchor 会改变,会影响 resolve,所以 suspense 实例的属性也要进行修改,这时还有很重要的一部分 patchSuspense,但是跟原理相关性较小,就不写了

runtime-core/renderer.js
1const processComponent = (n1, n2, container, isSVG, anchor) => {
2 if (n1 == null) {
3 // ...
4 if (isPromise(render)) {
5 const suspense = getParentSuspense(n2)
6 const placeholder = instance.subTree = h(TextType, { nodeValue: '' })
7 patch(null, placeholder, container, anchor)
8 suspense.register(
9 instance,
10 () => setupRenderEffect(
11 instance,
12 internals.renderOptions.parentNode(instance.subTree.node),
13 isSVG,
14 internals.renderOptions.nextSibling(instance.subTree.node),
15 ),
16 )
17 } else if (isFunction(render)) {
18 setupRenderEffect(instance, container, isSVG, anchor)
19 } else {
20 console.warn('setup component: ', n2.type, ' need to return a render function')
21 }
22
23 function setupRenderEffect(instance, container, isSVG, anchor) {
24 instance.update = effect(() => { // component update 的入口
25 const renderResult = instance.render()
26 const vnode = instance.vnode
27 vnode.children = [renderResult]
28 renderResult.parent = vnode
29 patch(instance.subTree, renderResult, container, isSVG, anchor)
30 instance.subTree = renderResult
31 }, {
32 scheduler: queueJob,
33 })
34 }
35 } // ...
36}

接下来对于 async setup 子组件的处理就要修改 runtime 了,我们对 setup 返回结果进行判断,如果是 Promise 就找到 parentSuspense 进行注册,这里我们抽离 setupRenderEffect,注册时传入一个回调函数,用于 suspense resolve 时继续渲染该子组件使用,同时创建一个 placeholder 给组件站位,用以 setupRenderEffect 中获取 container 和 anchor,因为 async setup 组件在没有 resolve 时可能有新的节点插入,如果 container、anchor 还是旧的值时可能会出错(anchor 为 null,但是之后插入了节点,resolve 时 anchor 还是 null 的话就导致节点顺序错误)

runtime-core/components/suspense.js
1const createSuspense = (vnode, container, isSVG, anchor, internals, hiddenContainer) => {
2 const suspense = {
3 deps: [],
4 // ...
5 register(instance, setupRenderEffect) {
6 suspense.deps.push(instance)
7 instance.render
8 .catch(e => {
9 handleError(e, instance)
10 })
11 .then(renderFn => {
12 instance.render = renderFn
13 setupRenderEffect()
14 const index = suspense.deps.indexOf(instance)
15 suspense.deps.splice(index, 1)
16 if (suspense.deps.length === 0) {
17 suspense.resolve()
18 }
19 })
20 },
21 }
22 return suspense
23}

然后 register 就是将 async setup 组件实例加入到 suspense.deps 中,然后等 render resolve 时调用 setupRenderEffect 渲染该组件,并判断是否可以 resolve 了,这里 catch 后 handleError 是因为 async setup 可以执行副作用,可能会出错

defineAsyncComponent

是一个高阶组件,相当于一个增强版的 lazy,当它的上层有 Suspense 时,就返回一个 Promise,否则返回相应状态的组件

runtime-core/api-define-component.js
1export const defineAsyncComponent = (options) => {
2 if (isFunction(options)) options = { loader: options }
3
4 const {
5 loader,
6 errorComponent,
7 suspensible = true,
8 onError,
9 } = options
10
11 let resolvedComponent = null
12
13 let retries = 0
14 const retry = () => {
15 retries++
16 return load()
17 }
18
19 const load = () => loader()
20 .catch(e => {
21 if (onError) {
22 return new Promise((resolve, reject) => {
23 onError(
24 e,
25 () => resolve(retry()),
26 () => reject(e),
27 retries,
28 )
29 })
30 } else {
31 throw e
32 }
33 })
34 .then((comp) => {
35 if (comp && (comp.__esModule || comp[Symbol.toStringTag] === 'Module')) {
36 comp = comp.default
37 }
38 resolvedComponent = comp
39 return comp
40 })
41
42 return defineComponent((props) => {
43 const instance = getCurrentInstance()
44 if (resolvedComponent) return () => h(resolvedComponent, props)
45 if (suspensible && getParentSuspense(instance.vnode)) {
46 return load()
47 .then(comp => {
48 return () => h(comp, props)
49 })
50 .catch(e => {
51 handleError(e, instance)
52 return () => errorComponent ? h(errorComponent, { error: e }) : null
53 })
54 }
55 })
56}

先来看有 Suspense 的情况,类似于 lazy 的实现,作为一个高阶组件返回 Promise,在出错的时候如果有 onError 就通过 onError 交给用户处理,没有就继续抛出 error,后面 catch 住渲染 errorComponent

再补上没有 Suspense 的情况

runtime-core/api-define-component.js
1export const defineAsyncComponent = (options) => {
2 if (isFunction(options)) options = { loader: options }
3
4 const {
5 loader,
6 loadingComponent,
7 errorComponent,
8 delay = 200,
9 timeout,
10 suspensible = true,
11 onError,
12 } = options
13 // ...
14
15 return defineComponent((props) => {
16 // ...
17
18 const error = ref()
19 const loading = ref(true)
20 const delaying = ref(!!delay) // 延后出现 LoadingComponent
21
22 if (delay) {
23 setTimeout(() => delaying.value = false, delay)
24 }
25 if (timeout) {
26 setTimeout(() => {
27 // 超时
28 if (loading.value && !error.value) {
29 const err = new Error(`Async component timed out after ${timeout}ms.`)
30 handleError(err, instance)
31 error.value = err
32 }
33 }, timeout)
34 }
35
36 load()
37 .then(() => loading.value = false)
38 .catch(e => {
39 handleError(e, instance)
40 error.value = e
41 })
42
43 return () => {
44 if (!loading.value && resolvedComponent) return h(resolvedComponent, props)
45 // loading.value === true
46 else if (error.value && errorComponent) return h(errorComponent, { error: error.value })
47 else if (loadingComponent && !delaying.value) return h(loadingComponent)
48 return null
49 }
50 })
51}

这里通过判断 suspensible 为 false 或者没有 parentSuspense 返回 render function,根据相应的状态渲染相应的组件,delay 这个参数的作用是为了 delay 出现 loadingComponent 的,如果加载比较快就不用展示 loading

我们目前写的不能实现的一种情况是 suspensible 为 false 但是有 parentSuspense

1const ProfileDetails = defineAsyncComponent({
2 loader: () => import('./async.jsx'),
3 loadingComponent: defineComponent(() => () => <h1>Loading...</h1>),
4 suspensible: false,
5});
6
7const App = {
8 setup(props) {
9 return () => (
10 <Suspense fallback={<h1>Loading by Suspense</h1>}>
11 <ProfileDetails />
12 </Suspense>
13 );
14 },
15};

这是因为 setupRenderEffect 传入的 container、anchor 是不变的,通过闭包存起来了,ProfileDetails 一开始渲染时是在 Suspense 中的,它的 container 是 hiddenContainer,之后渲染也是 hiddenContainer,所以导致页面空白,我们可以把 container、anchor 放到 instance 实例上,让这两个值可以改变,通过 instance 上的 container、anchor 进行渲染

KeepAlive

建立一个 Map 作为缓存,以子节点的 key 或 type 作为缓存的 key(const key = vnode.key == null ? vnode.type : vnode.key);KeepAlive 的 render function 被调用时,也就是 KeepAlive 被渲染时,会根据 props 的 includes 和 excludes 规则判断 children 是否可以被缓存,不可以就直接渲染,可以就在缓存里找,如果缓存里有就用缓存中的进行渲染,children 的状态都是旧的在缓存中的,否则用新的 children 并进行缓存

1if (cachedVNode) {
2 // copy over mounted state
3 vnode.el = cachedVNode.el
4 vnode.component = cachedVNode.component
5 if (vnode.transition) {
6 // recursively update transition hooks on subTree
7 setTransitionHooks(vnode, vnode.transition!)
8 }
9 // avoid vnode being mounted as fresh
10 vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
11 // make this key the freshest
12 keys.delete(key)
13 keys.add(key)
14} else {
15 keys.add(key)
16 // prune oldest entry
17 if (max && keys.size > parseInt(max as string, 10)) {
18 pruneCacheEntry(keys.values().next().value)
19 }
20}

源码中 KeepAlive 的缓存用到了 LRU 算法,keys 是一个 Set,可以看到每次使用缓存时会刷新一下缓存,变成新鲜的,如果再来新缓存时,缓存超过了 max,就删去最陈旧的缓存,利用 Set 对 LRU 进行了简易的实现

Transition

Transition 是通过给 DOM 节点在合适时机添加移除 CSS 类名实现的,对于不同平台有不同的实现方法,Transiton 是针对浏览器平台对 BaseTransition 的封装

1// DOM Transition is a higher-order-component based on the platform-agnostic
2// base Transition component, with DOM-specific logic.
3export const Transition: FunctionalComponent<TransitionProps> = (
4 props,
5 { slots }
6) => h(BaseTransition, resolveTransitionProps(props), slots)
7
8export function resolveTransitionProps(
9 rawProps: TransitionProps
10): BaseTransitionProps<Element> {
11 // 拿到对应的 CSS 类名
12 let {
13 name = 'v',
14 type,
15 css = true,
16 duration,
17 enterFromClass = `${name}-enter-from`,
18 enterActiveClass = `${name}-enter-active`,
19 enterToClass = `${name}-enter-to`,
20 appearFromClass = enterFromClass,
21 appearActiveClass = enterActiveClass,
22 appearToClass = enterToClass,
23 leaveFromClass = `${name}-leave-from`,
24 leaveActiveClass = `${name}-leave-active`,
25 leaveToClass = `${name}-leave-to`
26 } = rawProps
27 // ...
28 // 重写 hooks 回调函数,根据对应的添加或移除 CSS 类名
29 return extend(baseProps, {
30 onBeforeEnter(el) {
31 onBeforeEnter && onBeforeEnter(el)
32 addTransitionClass(el, enterActiveClass)
33 addTransitionClass(el, enterFromClass)
34 },
35 onBeforeAppear(el) {
36 onBeforeAppear && onBeforeAppear(el)
37 addTransitionClass(el, appearActiveClass)
38 addTransitionClass(el, appearFromClass)
39 },
40 // ...
41 } as BaseTransitionProps<Element>)
42}

BaseTransition 做的就是从 props 传入的 hooks 通过 resolveTransitionHooks 进一步进行封装,封装成针对 diff 阶段各个时机进行调用的 hooks(beforeEnter、enter、leave、afterLeave、delayLeave、clone),setTransitionHooks 就是把这些 hooks 放到 vnode 上,以便在 diff 过程中进行调用

1const BaseTransitionImpl = {
2 name: `BaseTransition`,
3
4 props: {
5 // ...
6 },
7
8 setup(props: BaseTransitionProps, { slots }: SetupContext) {
9 const instance = getCurrentInstance()!
10 const state = useTransitionState()
11 // ...
12
13 return () => {
14 const children =
15 slots.default && getTransitionRawChildren(slots.default(), true)
16 // ...
17
18 // at this point children has a guaranteed length of 1.
19 const child = children[0]
20 // ...
21
22 // in the case of <transition><keep-alive/></transition>, we need to
23 // compare the type of the kept-alive children.
24 const innerChild = getKeepAliveChild(child)
25 if (!innerChild) {
26 return emptyPlaceholder(child)
27 }
28
29 const enterHooks = resolveTransitionHooks(
30 innerChild,
31 rawProps,
32 state,
33 instance
34 )
35 setTransitionHooks(innerChild, enterHooks)
36
37 const oldChild = instance.subTree
38 const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
39 // ...
40 if (
41 oldInnerChild &&
42 oldInnerChild.type !== Comment &&
43 (!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged)
44 ) {
45 const leavingHooks = resolveTransitionHooks(
46 oldInnerChild,
47 rawProps,
48 state,
49 instance
50 )
51 // update old tree's hooks in case of dynamic transition
52 setTransitionHooks(oldInnerChild, leavingHooks)
53 // ...
54 }
55
56 return child
57 }
58 }
59}

调用的时机就是有关 vnode 节点位置改变的时候,分别是 mount、move 和 unmount。mount 时就调用 BeforeEnter,并注册 enter 到 post 任务队列中;unmount 时就调用 leave 和 afterLeave,并注册 delayLeave 到 post 任务队列中;move 根据 moveType 的不同调用的也不同,比如 Suspense 中 resolve 时是把 children 从 hiddContainer 移到 container 中,相当于 mount,KeepAlive 的 activate 相当于 mount,deactivate 相当于 unmount

Ref

ref(指 runtime 的 ref)是用来拿到宿主环境的节点实例或者组件实例的

runtime-core/renderer.js
1const setRef = (ref, oldRef, vnode) => {
2 // unset old ref
3 if (oldRef != null && oldRef !== ref) {
4 if (isRef(oldRef)) oldRef.value = null
5 }
6 // set new ref
7 const value = getRefValue(vnode)
8 if (isRef(ref)) {
9 ref.value = value
10 } else if (isFunction(ref)) {
11 callWithErrorHandling(ref, getParentInstance(vnode), [value])
12 } else {
13 console.warn('Invalid ref type:', value, `(${typeof value})`)
14 }
15}
16
17const getRefValue = (vnode) => {
18 const { type } = vnode
19 if (isSetupComponent(type)) return vnode.instance
20 if (isString(type) || isTextType(type)) return vnode.node
21 return type.getRefValue(internals, { vnode })
22}

ref 的更新由于传入的 ref(指响应式 ref 用来接收实例)可能不同(<img ref={num % 2 ? imgRef1 : imgRef2} />),所以要先清空 oldRef,再赋值 newRef

runtime-core/renderer.js
1const patch = (n1, n2, container, isSVG, anchor = null) => {
2 // ...
3 if (n2.ref != null) {
4 setRef(n2.ref, n1?.ref ?? null, n2)
5 }
6}
7
8const unmount = (vnode, doRemove = true) => {
9 const { type, ref } = vnode
10 if (ref != null) {
11 setRef(ref, null, vnode)
12 }
13 // ...
14}

ref 的更新主要在两个地方,一个是在 patch 之后,也就是更新 DOM 节点或组件实例之后,保证拿到最新的值,另一个是在 unmount 移除节点之前

Complier 优化

没有比 Vue3 Compiler 优化细节,如何手写高性能渲染函数这篇写的更好的了

这里简单说一下原理

  1. Block Tree 和 PatchFlags

    编译时生成的代码会打上 patchFlags,用来标记动态部分的信息

    1export const enum PatchFlags {
    2 // Indicates an element with dynamic textContent (children fast path)
    3 TEXT = 1,
    4
    5 // Indicates an element with dynamic class binding.
    6 CLASS = 1 << 1,
    7
    8 // Indicates an element with dynamic style
    9 STYLE = 1 << 2,
    10
    11 // Indicates an element that has non-class/style dynamic props.
    12 // Can also be on a component that has any dynamic props (includes
    13 // class/style). when this flag is present, the vnode also has a dynamicProps
    14 // array that contains the keys of the props that may change so the runtime
    15 // can diff them faster (without having to worry about removed props)
    16 PROPS = 1 << 3,
    17
    18 // Indicates an element with props with dynamic keys. When keys change, a full
    19 // diff is always needed to remove the old key. This flag is mutually
    20 // exclusive with CLASS, STYLE and PROPS.
    21 FULL_PROPS = 1 << 4,
    22
    23 // Indicates an element with event listeners (which need to be attached during hydration)
    24 HYDRATE_EVENTS = 1 << 5,
    25
    26 // Indicates a fragment whose children order doesn't change.
    27 STABLE_FRAGMENT = 1 << 6,
    28
    29 // Indicates a fragment with keyed or partially keyed children
    30 KEYED_FRAGMENT = 1 << 7,
    31
    32 // Indicates a fragment with unkeyed children.
    33 UNKEYED_FRAGMENT = 1 << 8,
    34
    35 // ...
    36}

    创建的 Block 也会有 dynamicProps、dynamicChildren 表示动态的部分,Block 也是一个 VNode,只不过它有这些动态部分的信息

    dynamicChildren 中即包含 children 中动态的部分,也包含 children 中的 Block,这样 Block 层层连接形成 Block Tree,在更新的时候只更新动态的那一部分

    1const patchElement = (
    2 n1: VNode,
    3 n2: VNode,
    4 parentComponent: ComponentInternalInstance | null,
    5 parentSuspense: SuspenseBoundary | null,
    6 isSVG: boolean,
    7 optimized: boolean
    8) => {
    9 const el = (n2.el = n1.el!)
    10 let { patchFlag, dynamicChildren, dirs } = n2
    11 // #1426 take the old vnode's patch flag into account since user may clone a
    12 // compiler-generated vnode, which de-opts to FULL_PROPS
    13 patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
    14 const oldProps = n1.props || EMPTY_OBJ
    15 const newProps = n2.props || EMPTY_OBJ
    16 // ...
    17
    18 if (patchFlag > 0) {
    19 // the presence of a patchFlag means this element's render code was
    20 // generated by the compiler and can take the fast path.
    21 // in this path old node and new node are guaranteed to have the same shape
    22 // (i.e. at the exact same position in the source template)
    23 if (patchFlag & PatchFlags.FULL_PROPS) {
    24 // element props contain dynamic keys, full diff needed
    25 patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
    26 } else {
    27 // class
    28 // this flag is matched when the element has dynamic class bindings.
    29 if (patchFlag & PatchFlags.CLASS) {
    30 if (oldProps.class !== newProps.class) {
    31 hostPatchProp(el, 'class', null, newProps.class, isSVG)
    32 }
    33 }
    34
    35 // style
    36 // this flag is matched when the element has dynamic style bindings
    37 if (patchFlag & PatchFlags.STYLE) {
    38 hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
    39 }
    40
    41 // props
    42 // This flag is matched when the element has dynamic prop/attr bindings
    43 // other than class and style. The keys of dynamic prop/attrs are saved for
    44 // faster iteration.
    45 // Note dynamic keys like :[foo]="bar" will cause this optimization to
    46 // bail out and go through a full diff because we need to unset the old key
    47 if (patchFlag & PatchFlags.PROPS) {
    48 // if the flag is present then dynamicProps must be non-null
    49 const propsToUpdate = n2.dynamicProps!
    50 for (let i = 0; i < propsToUpdate.length; i++) {
    51 const key = propsToUpdate[i]
    52 const prev = oldProps[key]
    53 const next = newProps[key]
    54 if (next !== prev || (hostForcePatchProp && hostForcePatchProp(el, key))) {
    55 hostPatchProp(el, key, prev, next, isSVG, n1.children as VNode[], parentComponent, parentSuspense, unmountChildren)
    56 }
    57 }
    58 }
    59 }
    60
    61 // text
    62 // This flag is matched when the element has only dynamic text children.
    63 if (patchFlag & PatchFlags.TEXT) {
    64 if (n1.children !== n2.children) {
    65 hostSetElementText(el, n2.children as string)
    66 }
    67 }
    68 } else if (!optimized && dynamicChildren == null) {
    69 // unoptimized, full diff
    70 patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
    71 }
    72
    73 if (dynamicChildren) {
    74 patchBlockChildren(n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense)
    75 } else if (!optimized) {
    76 // full diff
    77 patchChildren(n1, n2, el, null, parentComponent, parentSuspense)
    78 }
    79
    80 // ...
    81}
  2. 静态提升

    以下是 Vue 3 Template Explorer 选上 hoistStatic 这个选项后编译出的代码

    1<div>
    2 <p>text</p>
    3</div>
    1import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
    2
    3const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "text", -1 /* HOISTED */)
    4
    5export function render(_ctx, _cache, $props, $setup, $data, $options) {
    6 return (_openBlock(), _createBlock("div", null, [
    7 _hoisted_1
    8 ]))
    9}

    可以看到 <p>text</p> 生成的是 _hoisted_1 变量,在 render 作用域外面,这样每次 render 函数调用是就可以服用 _hoisted_1,减少 VNode 创建的性能消耗

  3. 预字符串化

    1<div>
    2 <p>text</p>
    3 <p>text</p>
    4 <p>text</p>
    5 <p>text</p>
    6 <p>text</p>
    7 <p>text</p>
    8 <p>text</p>
    9 <p>text</p>
    10 <p>text</p>
    11 <p>text</p>
    12</div>
    1import { createVNode as _createVNode, createStaticVNode as _createStaticVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
    2
    3const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p><p>text</p>", 10)
    4
    5export function render(_ctx, _cache, $props, $setup, $data, $options) {
    6 return (_openBlock(), _createBlock("div", null, [
    7 _hoisted_1
    8 ]))
    9}

    当有大量连续的静态的节点时,相比静态提升,预字符串化会进一步进行优化,通过字符串创建 Static VNode

    1const patch: PatchFn = (
    2 n1,
    3 n2,
    4 container,
    5 anchor = null,
    6 parentComponent = null,
    7 parentSuspense = null,
    8 isSVG = false,
    9 optimized = false
    10) => {
    11 // ...
    12 const { type, ref, shapeFlag } = n2
    13 switch (type) {
    14 // ...
    15 case Static:
    16 if (n1 == null) {
    17 mountStaticNode(n2, container, anchor, isSVG)
    18 } else if (__DEV__) {
    19 patchStaticNode(n1, n2, container, isSVG)
    20 }
    21 break
    22 // ...
    23 }
    24 // ...
    25}
    26
    27const mountStaticNode = (n2: VNode, container: RendererElement, anchor: RendererNode | null, isSVG: boolean) => {
    28 // static nodes are only present when used with compiler-dom/runtime-dom
    29 // which guarantees presence of hostInsertStaticContent.
    30 ;[n2.el, n2.anchor] = hostInsertStaticContent!(n2.children as string, container, anchor, isSVG)
    31}

    Static VNode 会在 patch 是直接插入到 container 中,生产环节下不进行更新

    预字符串化的好处有生成代码的体积减少、减少创建 VNode 的开销、减少内存占用

😃 ramble

Vue3 源码系列结束!

Vue3 目前写的只是它的响应式系统和运行时,还有很大的一个部分 complier,这一部分由于我对编译目前还没有太多的了解,而且对于理解 Vue3 核心原理影响并不大,所以就没有写,以后可能会写一写吧

之后就是 React 的源码了,至于我为什么热衷于看源码,不仅是因为自己的学习习惯,也是因为这些框架的源码相当于前端的“边界”,不仅代表着挑战也代表着我这一技术方向的深度

simple-vue 实现完整代码