携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情 >>
版本:3.2.31
Vue 在处理虚拟DOM的更新时,会对新旧两个 VNode 节点通过 Diff 算法进行比较,然后通过对比结果找出差异的节点或属性进行按需更新。这个 Diff 过程,在 Vue 中叫作 patch 过程,patch 的过程就是以新的 VNode 为基准,去更新旧的 VNode。
接下来,我们通过源码来看看 patch 过程中做了哪些事情。
patch 函数
// packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
n1, // 旧虚拟节点
n2, // 新虚拟节点
container,
anchor = null, // 定位锚点DOM,用于往锚点前插入节点
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren // 是否启用 diff 优化
) => {
// 新旧虚拟节点相同,直接返回,不做 Diff 比较
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
// 新旧虚拟节点不相同(key 和 type 不同),则卸载旧的虚拟节点及其子节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
// 卸载旧的虚拟节点及其子节点
unmount(n1, parentComponent, parentSuspense, true)
// 将 旧虚拟节点置为 null,保证后面走整个节点的 mount 逻辑
n1 = null
}
// PatchFlags.BAIL 标志用于指示应该退出 diff 优化
if (n2.patchFlag === PatchFlags.BAIL) {
// optimized 置为 false ,在后续的 Diff 过程中不会启用 diff 优化
optimized = false
// 将新虚拟节点的动态子节点数组置为 null,则不会进行 diff 优化
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text: // 处理文本
processText(n1, n2, container, anchor)
break
case Comment: // 处理注释
processCommentNode(n1, n2, container, anchor)
break
case Static: // 处理静态节点
if (n1 == null) {
// 挂载静态节点
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
// 更新静态节点
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment: // 处理 Fragment 元素
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理 ELEMENT 类型的 DOM 元素
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理组件
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理 Teleport 组件
// 调用 Teleport 组件内部的 process 函数,渲染 Teleport 组件的内容
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理 Suspense 组件
// 调用 Suspense 组件内部的 process 函数,渲染 Suspense 组件的内容
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
// 设置 ref 引用
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
从上面的源码中,我们可以清晰地看到 patch 过程所做的事情:
- 如果新旧虚拟节点相同 (n1 === n2),则直接返回,不做 Diff 比较。
- 如果新旧虚拟节点不相同,则直接卸载旧的虚拟节点及其子节点。同时将旧虚拟节点n1 置为 null,这样就保证了新节点可以正常挂载。
- 判断新虚拟节点的 patchFlag 类型是否为 PatchFlags.BAIL,则将 optimized 置为 false,那么在后续的 Diff 过程中就不会启用 diff 优化。同时也将新虚拟节点的动态子节点数组 dynamicChildren 置为 null,在后续 Diff 过程中也不会启用 diff 优化。
- 然后根据新虚拟节点的 type 类型,分别对文本节点、注释节点、静态节点以及Fragment节点调用相应的处理函数对其进行处理。
- 接着根据 shapeFlag 的类型,调用不同的处理函数,分别对 Element类型的节点、Component 组件、Teleport 组件、Suspense 异步组件进行处理。
- 最后,调用了 setRef 函数来设置 ref 引用。
processText 处理文本节点
// packages/runtime-core/src/renderer.ts
// 处理文本
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
// 首次挂载时,新建一个文本节点到 container 中
// 并将文本元素存储到新虚拟节点的 el 属性上
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
)
} else {
// 获取旧虚拟节点的真实 DOM 元素
// 同时将新虚拟节点的 el 指向旧虚拟节点指向的真实 DOM 元素
const el = (n2.el = n1.el!)
// .children 就是文本内容
// 新旧节点的文本内容不同,则将真实 DOM 元素的文本内容更新为新的文本内容
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
在处理文本节点时,如果 n1 为 null,即在首次挂载时调用 hostInsert 方法,即调用原生DOM API insertBefore 方法,将新建的文本元素插入到 container 中,同时将文本元素存储到新虚拟节点 n2 的 el 属性上,保持对真实DOM元素的引用。
如果 n1 不为null,说明是在更新阶段,此时判断新旧节点的文本内容是否相同,如果不同,则调用 hostSetText 方法将真实 DOM 元素的文本内容更新为新的文本内容。
processCommentNode 处理注释节点
// packages/runtime-core/src/renderer.ts
// 处理注释
const processCommentNode: ProcessTextOrCommentFn = (
n1,
n2,
container,
anchor
) => {
if (n1 == null) {
// 首次挂载时,新建一个注释节点到 container 中
// 并将注释节点存储到新虚拟节点的 el 属性上
hostInsert(
(n2.el = hostCreateComment((n2.children as string) || '')),
container,
anchor
)
} else {
// there's no support for dynamic comments
// 将新虚拟节点的 el 置为旧虚拟节点的 el
n2.el = n1.el
}
}
可以看到,处理注释节点的思路和处理文本节点的思路相似。在首次挂载时调用 hostInsert 方法,将新建的注释插入到 container 中,同时将文本元素存储到新虚拟节点 n2 的 el 属性上,保持对真实DOM元素的引用。在更新阶段,则是直接将新虚拟节点的 el 设置为旧虚拟节点的 el。
mountStaticNode/patchStaticNode 处理静态节点
// packages/runtime-core/src/renderer.ts
// 挂载静态节点
const mountStaticNode = (
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
isSVG: boolean
) => {
// static nodes are only present when used with compiler-dom/runtime-dom
// which guarantees presence of hostInsertStaticContent.
// 向 container 中插入一个静态节点
;[n2.el, n2.anchor] = hostInsertStaticContent!(
n2.children as string,
container,
anchor,
isSVG,
n2.el,
n2.anchor
)
}
/**
* Dev / HMR only
*/
// 仅用于开发环境 热更新
const patchStaticNode = (
n1: VNode,
n2: VNode,
container: RendererElement,
isSVG: boolean
) => {
// static nodes are only patched during dev for HMR
if (n2.children !== n1.children) {
const anchor = hostNextSibling(n1.anchor!)
// remove existing
// 移除已经存在的静态节点
removeStaticNode(n1)
// insert new
// 插入一个新的静态节点
;[n2.el, n2.anchor] = hostInsertStaticContent!(
n2.children as string,
container,
anchor,
isSVG
)
} else {
// 新虚拟节点的 el 指向 旧虚拟节点的 el
n2.el = n1.el
// 新虚拟节点的 anchor 指向 旧虚拟节点的 anchor
n2.anchor = n1.anchor
}
}
可以看到,挂载静态节点时,调用了hostInsertStaticContent 函数向 container 中插入一个静态节点。对于静态节点的更新,只在开发环境时做更新处理。在做更新处理时,先移除已经存在的静态节点,然后再往 container 中插入一个新的静态节点。
processFragment 处理 Fragment 元素
// packages/runtime-core/src/renderer.ts
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
//
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
if (__DEV__ && isHmrUpdating) {
// HMR updated, force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
}
// check if this is a slot fragment with :slotted scope ids
if (fragmentSlotScopeIds) {
slotScopeIds = slotScopeIds
? slotScopeIds.concat(fragmentSlotScopeIds)
: fragmentSlotScopeIds
}
if (n1 == null) {
// 首次挂载时插入 Fragment
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// a fragment can only have array children
// since they are either generated by the compiler, or implicitly created
// from arrays.
// 挂载子节点,这里只能是数组的子集
mountChildren(
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
// a stable fragment (template root or <template v-for>) doesn't need to
// patch children order, but it may contain dynamicChildren.
// 稳定的 Fragment (例如:template root or <template v-for>) 不需要更新整个 block
// 但是可能还会包含动态子节点,因此需要对动态子节点进行更新
patchBlockChildren(
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds
)
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2)
} else if (
// #2080 if the stable fragment has a key, it's a <template v-for> that may
// get moved around. Make sure all root level vnodes inherit el.
// #2134 or if it's a component root, it may also get moved around
// as the component is being moved.
n2.key != null ||
(parentComponent && n2 === parentComponent.subTree)
) {
// 转换静态子节点
traverseStaticChildren(n1, n2, true /* shallow */)
}
} else {
// keyed / unkeyed, or manual fragments.
// for keyed & unkeyed, since they are compiler generated from v-for,
// each child is guaranteed to be a block so the fragment will never
// have dynamicChildren.
// 更新子节点
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
在首次渲染 DOM 树时,创建 FragmentStart 和 FragmentEnd,并将它们插入到 container 中,然后调用 mountChildren 挂载 Fragment 的所有子节点。
在更新阶段,Fragment 是稳定的,并且存在动态子节点,则调用 patchBlockChildren 函数对子节点进行更新,否则直接调用 patchChildren 函数更新子节点。
processElement 处理 Element
// packages/runtime-core/src/renderer.ts
// 处理 ELEMENT 类型的 DOM 元素
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
// 挂载 Element 节点
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 更新 Element 节点
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
从源码中可以看到,在首次渲染 DOM 树时,调用 mountElement 函数挂载 Element 节点。在更新阶段,则调用 patchElement 函数来更新 Element 节点。
mountElement 挂载 Element 节点
// packages/runtime-core/src/renderer.ts
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
if (
!__DEV__ &&
vnode.el &&
hostCloneNode !== undefined &&
patchFlag === PatchFlags.HOISTED
) {
// If a vnode has non-null el, it means it's being reused.
// Only static vnodes can be reused, so its mounted DOM nodes should be
// exactly the same, and we can simply do a clone here.
// only do this in production since cloned trees cannot be HMR updated.
// 复用静态节点
el = vnode.el = hostCloneNode(vnode.el)
} else {
// 创建 DOM 节点
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 设置 DOM节点的文本内容
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 挂载子节点
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
// 处理指令
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
// 处理 DOM 节点上的 props
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
// 对 props 进行 diff
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
/**
* Special case for setting value on DOM elements:
* - it can be order-sensitive (e.g. should be set *after* min/max, #2325, #4024)
* - it needs to be forced (#1471)
* #2353 proposes adding another renderer option to configure this, but
* the properties affects are so finite it is worth special casing it
* here to reduce the complexity. (Special casing it also should not
* affect non-DOM renderers)
*/
if ('value' in props) {
// 对DOM节点上的 value 属性进行diff,如<select value>
hostPatchProp(el, 'value', null, props.value)
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// scopeId
// 设置 DOM 的一些 attr 属性
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
Object.defineProperty(el, '__vnode', {
value: vnode,
enumerable: false
})
Object.defineProperty(el, '__vueParentComponent', {
value: parentComponent,
enumerable: false
})
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
// #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
// #1689 For inside suspense + suspense resolved case, just call it
// 执行动画相关的生命周期钩子
// 判断一个 VNode 是否需要过渡
const needCallTransitionHooks =
(!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
transition &&
!transition.persisted
if (needCallTransitionHooks) {
// 在挂载DOM元素之前执行动画的 beforeEnter 生命周期钩子函数
transition!.beforeEnter(el)
}
// 挂载DOM元素
hostInsert(el, container, anchor)
// 挂载完DOM元素后,执行动画的 enter 生命周期钩子函数
if (
(vnodeHook = props && props.onVnodeMounted) ||
needCallTransitionHooks ||
dirs
) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
// 调用 transition.enter 钩子,并把 DOM 元素作为参数传递
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
}
可以看到,在 mountElement 函数中:
- 如果DOM节点可复用,则调用 hostCloneNode 函数,对DOM节点进行复用,否则就调用 hostCreateElement 函数创建一个新的 DOM 节点,然后将其存储到虚拟节点的 el 属性上。
- 然后判断子节点的类型,如果子节点是文本,则调用 hostSetElementText 函数创建文本内容并将其插入到DOM节点中。如果子节点是一个数组,则调用 mountChildren 函数批量挂载子节点。
- 接下来设置DOM节点上的指令、props、attr 属性等。
- 最后是挂载 DOM 元素。在挂载DOM元素之前,判断该DOM元素上是否有过渡动效,如果有,则执行动画的 beforeEnter 生命周期钩子函数。然后调用 hostInsert 挂载 DOM 元素,挂载完 DOM 元素之后,执行动画的 enter 生命周期钩子函数,并将 DOM 元素作为参数传递。
mountChildren 挂载子节点
// packages/runtime-core/src/renderer.ts
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
start = 0
) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
// 递归调用 patch 函数,对子节点执行 diff 过程
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
从上面的代码中可以看到,在执行 mountChildren 挂载子节点上,实际上就是递归调用 patch 函数来对子节点执行 Diff 过程,对子节点进行挂载。
patchElement 更新 Element 节点
// packages/runtime-core/src/renderer.ts
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean // 是否启用 diff 优化
) => {
// 获取真实 DOM 元素,同时将新虚拟节点的 el 指向真实 DOM
const el = (n2.el = n1.el!)
let { patchFlag, dynamicChildren, dirs } = n2
// #1426 take the old vnode's patch flag into account since user may clone a
// compiler-generated vnode, which de-opts to FULL_PROPS
patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
let vnodeHook: VNodeHook | undefined | null
// disable recurse in beforeUpdate hooks
// 在 beforeUpdate 生命周期钩子函数中禁用 递归
parentComponent && toggleRecurse(parentComponent, false)
if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
}
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
// 在 beforeUpdate 生命周期钩子函数中启用 递归
parentComponent && toggleRecurse(parentComponent, true)
// 开发环境下,热更新,需要强制执行 diff
if (__DEV__ && isHmrUpdating) {
// HMR updated, force full diff
patchFlag = 0
optimized = false
dynamicChildren = null
}
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
// 存在动态子节点,对动态子节点执行 diff 过程
if (dynamicChildren) {
patchBlockChildren(
n1.dynamicChildren!,
dynamicChildren,
el,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds
)
if (__DEV__ && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2)
}
} else if (!optimized) {
// 不启用 diff 优化,那么所有子节点都要执行 diff 过程
// full diff
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
areChildrenSVG,
slotScopeIds,
false
)
}
if (patchFlag > 0) {
// the presence of a patchFlag means this element's render code was
// generated by the compiler and can take the fast path.
// in this path old node and new node are guaranteed to have the same shape
// (i.e. at the exact same position in the source template)
if (patchFlag & PatchFlags.FULL_PROPS) {
// element props contain dynamic keys, full diff needed
// 对 props 执行 diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
} else {
// class
// this flag is matched when the element has dynamic class bindings.
// 具有动态的 class,更新 class
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, isSVG)
}
}
// style
// this flag is matched when the element has dynamic style bindings
// 动态的 style,更新 style
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
}
// props
// This flag is matched when the element has dynamic prop/attr bindings
// other than class and style. The keys of dynamic prop/attrs are saved for
// faster iteration.
// Note dynamic keys like :[foo]="bar" will cause this optimization to
// bail out and go through a full diff because we need to unset the old key
// 处理动态属性/动态属性绑定
if (patchFlag & PatchFlags.PROPS) {
// if the flag is present then dynamicProps must be non-null
// 需要更新的动态 props
/**
* propsToUpdate是 onClick | onUpdate:modelValue 这些。
* 示例:<polygon :points="points"></polygon> propsToUpdate === ["points"]
*/
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
// 这里的 next 可能是 string、number、function、object、boolean
// #1471 force patch value
// props 的值不同,则执行更新
// 如果 props 是 value,则需要强制执行更新
if (next !== prev || key === 'value') {
// 更新 props
hostPatchProp(
el,
key,
prev,
next,
isSVG,
n1.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
}
}
// text
// This flag is matched when the element has only dynamic text children.
// 更新动态文本节点
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children as string)
}
}
} else if (!optimized && dynamicChildren == null) {
// 不启用 diff 优化,并且没用动态子节点,所有 props 要执行更新
// unoptimized, full diff
patchProps(
el,
n2,
oldProps,
newProps,
parentComponent,
parentSuspense,
isSVG
)
}
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
}
patchElement 函数主要用来更新 Element 节点,从上面的源码中可以看到:
- 首先从旧虚拟节点的 el 属性上获取该虚拟节点对应的真实 DOM 元素,并将新虚拟节点的 el 属性也指向该真实DOM元素。
- 然后判断新虚拟节点 n2 上是否存在动态子节点,如果存在,则调用 patchBlockChildren 函数对动态子节点执行 Diff 过程。如果在 Diff 的过程中没有启用 Diff 优化,则直接调用 patchChildren 函数更新所有子节点。
- 接着分别对Element 节点的动态属性 props、class、style以及动态的text进行更新。
patchBlockChildren 更新动态子节点
// packages/runtime-core/src/renderer.ts
const patchBlockChildren: PatchBlockChildrenFn = (
oldChildren,
newChildren,
fallbackContainer,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds
) => {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// Determine the container (parent element) for the patch.
const container =
// oldVNode may be an errored async setup() component inside Suspense
// which will not have a mounted element
oldVNode.el &&
// - In the case of a Fragment, we need to provide the actual parent
// of the Fragment itself so it can move its children.
(oldVNode.type === Fragment ||
// - In the case of different nodes, there is going to be a replacement
// which also requires the correct parent container
!isSameVNodeType(oldVNode, newVNode) ||
// - In the case of a component, it could contain anything.
oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
? hostParentNode(oldVNode.el)!
: // In other cases, the parent container is not actually used so we
// just pass the block element here to avoid a DOM parentNode call.
fallbackContainer
// 递归调用 patch 对动态子节点执行 diff
patch(
oldVNode,
newVNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
true
)
}
}
在 patchElement 函数中,如果新的虚拟节点 n2 上存在动态子节点,就会调用 patchBlockChildren 函数对动态子节点进行更新。从 patchBlockChildren 的源码可以看到,在对动态子节点进行更新时,实际上是递归调用 patch 函数来对动态子节点执行 Diff 过程,对动态子节点进行更新。
patchChildren 更新子节点
// packages/runtime-core/src/renderer.ts
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
// 旧节点的子节点
const c1 = n1 && n1.children
// 旧节点的 shapeFlag
const prevShapeFlag = n1 ? n1.shapeFlag : 0
// 新节点的子节点
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// fast path
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// this could be either fully-keyed or mixed (some keyed some not)
// presence of patchFlag means children are guaranteed to be arrays
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
return
}
}
// children has 3 possibilities: text, array or no children.
// 新子节点有 3 中可能:文本、数组、或没有 children
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 文本节点的快速 diff
// text children fast path
// 旧子节点是数组,则卸载旧子节点
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
// 子节点是文本节点,新旧文本不一致时,直接更新
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
// 子节点是数组时,对子节点进行 diff
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 旧子节点是数字
// prev children was array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 新子节点也是数组,对两组子节点进行 diff
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// no new children, just unmount old
// 旧子节点是数组时,没有新的子节点,删除旧子节点
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// prev children was text OR null
// new children is array OR null
// 旧子节点是文本或者 null
// 新子节点是数组或者为null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// mount new if array
// 新子节点是数组,则挂载新子节点
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
在 patchElement 函数中,如果参数 optimized 的值为 false,即不启用 Diff 优化,那么就会调用 patchChildren 函数对所有子节点执行 diff 过程,对子节点进行更新。在 patchChildren 函数中,开始涉及到 patch 过程中的核心 —— Diff 算法,这部分内容我们放在下一篇文章中详细解读。
processComponent 处理组件
// packages/runtime-core/src/renderer.ts
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
// 首次挂载时,// 判断当前要挂载的组件是否是 KeepAlive 组件
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
// 激活组件,即将隐藏容器中移动到原容器中
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// 不是 KeepAlive 组件,调用 mountComponent 挂载组件
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
// 更新阶段,直接更新组件
updateComponent(n1, n2, optimized)
}
}
从 processComponent 函数的源码中可以看到,在首先渲染 DOM 树时,需要判断当前挂载的组件是否是 KeepAlive 组件,如果是,则调用 KeepAlive 组件的内部方法 activate 方法激活组件,也就是将组将从隐藏容器中移动到原容器(页面) 中。如果不是 KeepAlive 组件,则调用 mountComponent 函数挂载组件。
而在更新阶段,则是直接调用 updateComponent 函数更新组件。
mountComponent 挂载组件
// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 2.x compat may pre-create the component instance before actually
// mounting
// 2.x 版本中可能在实际操作之前已经创建了组件实例
const compatMountInstance =
__COMPAT__ && initialVNode.isCompatRoot && initialVNode.component
// 1、创建组件实例
const instance: ComponentInternalInstance =
compatMountInstance ||
(initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 开发环境下注册热更新
if (__DEV__ && instance.type.__hmrId) {
registerHMR(instance)
}
if (__DEV__) {
pushWarningContext(initialVNode)
startMeasure(instance, `mount`)
}
// inject renderer internals for keepAlive
// 如果初始化的VNode是 KeepAlive 组件,则在组件实例的上下文中注入 renderer
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// resolve props and slots for setup context
if (!(__COMPAT__ && compatMountInstance)) {
if (__DEV__) {
startMeasure(instance, `init`)
}
// 设置组件实例
则在组件实例的上下文中注入 renderer(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
// setup() is async. This component relies on async logic to be resolved
// before proceeding
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
// Give it a placeholder if this is not hydration
// TODO handle self-defined fallback
if (!initialVNode.el) {
const placeholder = (instance.subTree = createVNode(Comment))
processCommentNode(null, placeholder, container!, anchor)
}
return
}
// 设置并且运行带有副作用的渲染函数
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
if (__DEV__) {
popWarningContext()
endMeasure(instance, `mount`)
}
}
在 mountComponent 函数中,做了以下事情:
- 首先是调用 createComponentInstance 函数创建组件实例。
- 然后判断即将挂载的组件是否是 KeepAlive 组件,如果是,则在组件实例的上下文中注入 renderer
- 接着设置组件实例,调用 setupComponent 函数初始化组件的 props、slots 。
- 最后调用 setupRenderEffect 函数,执行带有副作用的渲染函数。
setupComponent 初始化组件实例
// packages/runtime-core/src/component.ts
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children } = instance.vnode
// 是否是状态型组件
const isStateful = isStatefulComponent(instance)
// 初始化 props
initProps(instance, props, isStateful, isSSR)
// 初始化 slots
initSlots(instance, children)
// 仅为状态型组件挂载setup信息,非状态型组件仅为纯UI展示不需要挂载状态信息
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
可以看到,在 setupComponent 函数中:
- 首先调用 isStatefulComponent 函数判断当前组件是否是状态型组件。
- 然后分别初始化组件的 props、slots。
- 接下来执行 setupStatefulComponent 函数,为状态型组件挂载 setup 信息,而非状态型组件仅为纯UI展示,不需要挂载状态信息,因此此时 setupResult 的值应设置为 undefined。
- 最后将 setup 信息返回。
setupStatefulComponent 生成 setup 信息
// packages/runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 组件的 options,也就是 vnode.type
const Component = instance.type as ComponentOptions
if (__DEV__) {
if (Component.name) {
// 校验组件名称是否合法
validateComponentName(Component.name, instance.appContext.config)
}
if (Component.components) {
// 批量校验组件名称是否合法
const names = Object.keys(Component.components)
for (let i = 0; i < names.length; i++) {
validateComponentName(names[i], instance.appContext.config)
}
}
if (Component.directives) {
// 批量校验指令名称是否合法
const names = Object.keys(Component.directives)
for (let i = 0; i < names.length; i++) {
validateDirectiveName(names[i])
}
}
if (Component.compilerOptions && isRuntimeOnly()) {
warn(
`"compilerOptions" is only supported when using a build of Vue that ` +
`includes the runtime compiler. Since you are using a runtime-only ` +
`build, the options should be passed via your build tool config instead.`
)
}
}
// 0. create render proxy property access cache
// 创建渲染代理属性访问缓存
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
// 为组件实例创建渲染代理,同时将代理标记为 raw,
// 为的是在后续过程中不会被误转化为响应式数据,
// 渲染代理源对象是组件实例上下文
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 2. call setup()
// 调用 setup 函数
// 这里的setup是开发者调用 createApp 时传入的 setup 函数
const { setup } = Component
if (setup) {
// 创建 setup上下文并挂载到组件实例上
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 记录当前正在初始化的组件实例
setCurrentInstance(instance)
// 执行 setup 前暂停依赖收集
// PS: 执行setup期间是不允许进行依赖收集的,setup只是为了获取需要为组件提供的状态信息,在它里面不应该有其它非必要的副作用
// 真正的依赖收集等有较强副作用的操作应该放到 setup挂载之后,以免产生不可预测的问题
pauseTracking()
// 执行 setup 函数,并获得安装结果信息,setup执行结构就是我们定义的响应式数据、函数、钩子等
const setupResult = callWithErrorHandling(
setup, // 开发者调用 createApp 时定义的 setup函数
instance, // 根组件实例
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
// setup执行完毕后恢复依赖收集
resetTracking()
// 重置当前根组件实例
unsetCurrentInstance()
// 挂载 setup 执行的结果
// 在 SSR 服务端渲染或者 suspense 时 setup 返回的是 promise
// 因此需要判断 setupResult 是否是 promise,进行不同的操作
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
// 在SSR或者suspense时setup返回promise
// suspense因为有节点fallback,而setup中是正式渲染内容,因此是一个异步resolve的过程
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
} else if (__FEATURE_SUSPENSE__) {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`
)
}
} else {
// setupResult 返回的不是 promise
// 直接将 setup的执行结果挂载到组件实例上
handleSetupResult(instance, setupResult, isSSR)
}
} else {
finishComponentSetup(instance, isSSR)
}
}
从 setupStatefulComponent 函数的源码中可以看到,在开发环境下,会校验祖静的名称已经指令名称是否合法。
- 接下来为组件实例创建一个渲染代理属性accessCache,用于访问缓存。
- 接着继续为组件创建一个渲染代理 proxy,并同时经代理标记为 raw,为的是在后续过程中不会被误转化为响应式数据。渲染代理的源对象是组件实例的上下文对象。
- 接下来调用 setup 函数生成setup信息,这里的 setup 函数,就是开发者在调用 createApp 时传入的 setup 函数。
- 在执行 setup 的过程中,首先创建一个 setup 上下文对象,并将其挂载到组件实例上,然后调用 setCurrentInstance 函数记录当前正在初始化的组件实例。
- 在执行 setup 函数之前,需要先暂停依赖收集,原因是 setup 只是为了获取需要为组件提供的状态信息,在它里面不应该有其它非必要的副作用, 而真正的依赖收集等有较强副作用的操作应该放到 setup挂载之后,以免产生不可预测的问题。
- 在暂停依赖收集之后,执行 setup 函数,获得组件安装信息,这些安装信息就是开发者定义的响应式数据、函数、钩子等。
- setup 执行完毕后需要恢复依赖收集,因此调用 resetTracking 函数恢复依赖收集,并调用 unsetCurrentInstance 函数重置当前的组件实例。
- 如果是在服务端渲染或者是在 Suspense 组件中,setup 执行后返回的结果是一个 promise,因此我们还需要根据 setup 的结果是否是 promise ,执行不同的操作。如果是 promise ,则执行 promise 的 then 函数,获取真正的setup信息,将其挂载到组件实例上。如果不是 promise,则直接将 setup 执行后的结果挂载到组件实例上。
updateComponent 更新组件
// packages/runtime-core/src/renderer.ts
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
// 从旧虚拟节点上获取组件实例,并将组件实例添加到新虚拟节点上
const instance = (n2.component = n1.component)!
// 根据新旧虚拟节点VNode上的属性、指令、子节点等判断是否需要更新组件
// optimized 参数用于设置是否开启 diff 优化
if (shouldUpdateComponent(n1, n2, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
// async & still pending - just update props and slots
// since the component's reactive effect for render isn't set-up yet
if (__DEV__) {
pushWarningContext(n2)
}
// 异步组件,预更新组件
updateComponentPreRender(instance, n2, optimized)
if (__DEV__) {
popWarningContext()
}
return
} else {
// normal update
// 更新对应组件实例的 next 为新的 VNode
instance.next = n2
// in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect.
// 触发更新
instance.update()
}
} else {
// no update needed. just copy over properties
// 不需要更新,仅将旧虚拟节点上的属性等拷贝到新虚拟节点上
n2.component = n1.component
n2.el = n1.el
instance.vnode = n2
}
}
在 updateComponent 函数中,首先从旧虚拟节点 n1 的 component 属性获取当前需要更新的组件实例,并将该组件实例存储到新虚拟节点 n2 的 component 属性上,保持对组件实例的引用。
然后根据新旧 vnode 上的 props、指令、子节点等判断是否需要更新组件,如果需要更新组件,则调用组件实例的 update 方法触发更新。如果不需要更新,仅将旧虚拟节点上的属性等拷贝到新虚拟节点上即可。
Teleport.process 渲染 Teleport 组件
else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理 Teleport 组件
// 调用 Teleport 组件内部的 process 函数,渲染 Teleport 组件的内容
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
}
如上面的代码所示,如果 shapeFlag 的类型为 ShapeFlags.TELEPORT ,则调用 Teleport 组件内部的 process 函数,渲染 Teleport 组件的内容。Teleport 组件的源码解读请阅读《Vue3 源码解读之 Teleport 组件》一文。
Suspense.process 渲染 Suspense 组件
else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理 Suspense 组件
// 调用 Suspense 组件内部的 process 函数,渲染 Suspense 组件的内容
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
如上面的代码所示,如果 shapeFlag 的类型为 ShapeFlags.SUSPENSE ,则调用 Suspense 组件内部的 process 函数,渲染 Suspense 组件的内容。
总结
本文主要介绍了 patch 过程中的文本节点、注释节点、静态节点、Fragment节点、Element类型的节点、Component 组件、Teleport 组件、Suspense 异步组件等处理过程。
在调用 patchElement 更新 Element 类型的节点时,会调用 patchChildren 对子节点进行更新,在对子节点进行更新的过程中,需要对新旧子节点进行 Diff 比较。对于 Diff 算法的详细解读将放在下《Vue3 源码解读之patch算法(二)》一文中。