Vue 在处理虚拟DOM
的更新时,会对新旧两个 VNode
节点通过 Diff
算法进行比较,然后通过对比结果找出差异的节点或属性进行按需更新 。这个 Diff 过程,在 Vue 中叫作 patch
过程,patch
的过程就是以新的 VNode 为基准,去更新旧的 VNode。
接下来,我们通过源码来看看 patch
过程中做了哪些事情。
patch 函数
js
// 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
}
// 新旧虚拟节点不相同(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 处理文本节点
js
// packages/runtime-core/src/renderer.ts
// 处理文本 n1:旧虚拟节点 n2:新虚拟节点
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 处理注释节点
js
// 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 处理静态节点
js
// packages/runtime-core/src/renderer.ts
// 挂载静态节点
const mountStaticNode = (
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
isSVG: boolean
) => {
// 判断是否存在 hostInsertStaticContent 方法,该方法用于将静态节点插入到容器中。该方法只在使用 compiler-dom/runtime-dom 时才存在,因为这两个模块是用于浏览器环境的。
// 向 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
) => {
// 在开发环境下,如果修改了静态节点的内容,需要将其更新到页面上。为了提高性能,只有修改了静态节点的内容才会触发更新,而不是每次都重新渲染静态节点。
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 元素
js
// 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
}
// 插槽是一种组件通信机制,用于在父组件中定义模板,并在子组件中渲染。在处理插槽时,编译器需要根据插槽的特点来进行一些处理,例如提取作用域 ID
// 如果存在插槽片段的作用域 ID,则将其添加到插槽作用域 ID 数组中。
if (fragmentSlotScopeIds) {
slotScopeIds = slotScopeIds
? slotScopeIds.concat(fragmentSlotScopeIds)
: fragmentSlotScopeIds
}
if (n1 == null) {
// 首次挂载时插入 Fragment
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// 断言片段节点的子节点为数组类型,因为片段节点只能包含数组子节点。
// 挂载子节点,这里只能是数组的子集
mountChildren(
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// 如果旧节点存在动态子节点,则说明之前的片段可能是因为 renderSlot() 没有有效子节点而被跳过的
n1.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 判断新节点是否具有 key 属性。如果存在 key 属性,则说明该片段是一个带有 v-for 的模板,它的根节点可能会被移动,因此需要为其设置 el 属性
// #2134 判断新节点是否为组件的根节点。如果是组件的根节点,则说明该片段是一个组件的子树,它的根节点也可能会被移动,因此需要为其设置 el 属性。
n2.key != null ||
(parentComponent && n2 === parentComponent.subTree)
) {
// 转换静态子节点
traverseStaticChildren(n1, n2, true /* shallow */)
}
} else {
// 首先判断片段的子节点是否是编译器生成的。如果是编译器生成的,则子节点一定是块级别的节点,因此片段不会有动态子节点。
// 更新子节点
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
在首次渲染 DOM
树时,创建 FragmentStart
和 FragmentEnd
,并将它们插入到 container
中,然后调用 mountChildren
挂载 Fragment
的所有子节点。
在更新阶段,Fragment
是稳定的,并且存在动态子节点,则调用 patchBlockChildren
函数对子节点进行更新,否则直接调用 patchChildren
函数更新子节点。
processElement 处理 Element
js
// 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 节点
js
// 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
) {
// 首先判断虚拟节点的 el 属性是否存在。如果存在,则说明该虚拟节点是一个被复用的静态节点。
// 接下来,将虚拟节点的 el 属性赋值给 el 变量,并将虚拟节点的 el 属性设置为克隆节点的 el 属性。这样就可以复用静态节点了。
// 复用静态节点
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')
}
// 先判断 suspense 是否已经解析出来。如果已经解析出来,则直接调用 enter 钩子函数。
// 如果 suspense 还没有解析出来,则需要等待 suspense 解析出来之后再调用 enter 钩子函数。在这种情况下,需要在 suspense 解析出来之后再调用 enter 钩子函数
// 执行动画相关的生命周期钩子
// 判断一个 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 属性等。 - 然后先判断
suspense
是否已经解析出来。如果已经解析出来,则直接调用 enter 钩子函数。如果suspense
还没有解析出来,则需要等待suspense
解析出来之后再调用 enter 钩子函数。在这种情况下,需要在suspense
解析出来之后再调用 enter 钩子函数。 - 最后是挂载
DOM
元素。在挂载DOM
元素之前,判断该DOM
元素上是否有过渡动效,如果有,则执行动画的beforeEnter
生命周期钩子函数。然后调用hostInsert
挂载DOM
元素,挂载完DOM
元素之后,执行动画的enter
生命周期钩子函数,并将DOM
元素作为参数传递。
mountChildren 挂载子节点
js
// 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 节点
js
// 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 为什么需要考虑旧 vnode 的 patch flag?这是因为用户可能会克隆编译器生成的 vnode,从而使其 de-opt 到 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) {
// 首先判断元素的 patchFlag 是否包含 FULL_PROPS 标记。如果包含该标记,则可以采用快速更新的方式来更新元素。
// 接着,判断新旧节点是否具有相同的结构。如果节点结构相同,则可以直接更新元素的属性。否则,需要重新创建元素并更新属性。
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
// 元素的 prop/attr 可以是静态的或动态的。静态的 prop/attr 在编译期间就可以确定,而动态的 prop/attr 只能在运行时确定。在处理元素时,编译器需要根据元素的 prop/attr 是否动态来决定如何进行更新。
// 1、当元素具有除 class 和 style 之外的动态 prop/attr 绑定时,会设置的 patch flag。该标记表示元素具有动态 prop/attr 绑定,需要进行更新
// 2、解释了动态 prop/attr 绑定的键会被保存,以便快速遍历。这是因为在运行时,需要遍历动态 prop/attr 绑定的键来确定哪些 prop/attr 是动态的,从而进行更新
// 3、最后,当动态 prop/attr 绑定的键是动态的时,会放弃该优化,并执行全量更新。这是因为在更新时,需要取消旧键,再设置新键,无法通过快速更新来实现
// 处理动态属性/动态属性绑定
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 更新动态子节点
js
// 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]
// 先判断旧 vnode 是否具有 mounted element。如果有,则将其作为容器。否则,使用 fallbackContainer 作为容器。
// 接着,判断旧 vnode 的类型。如果旧 vnode 是 Fragment,则需要提供 Fragment 自身的实际父级元素作为容器,以便移动其子元素。如果旧 vnode 的类型与新 vnode 不同,则需要提供正确的父级容器。如果旧 vnode 是组件或 teleport,则其子元素可能包含任何内容,因此需要提供正确的父级容器。
// 最后,如果旧 vnode 的父级容器实际上没有使用,则只需将块元素传递给 host 函数,以避免调用 DOM parentNode
// 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
)
}
}
patch
是指将新的vnode
更新到旧的vnode
上的过程。在进行patch
时,需要确定新vnode
的容器(父元素)- 先判断旧
vnode
是否具有 mounted element。如果有,则将其作为容器。否则,使用fallbackContainer
作为容器。 - 接着,判断旧
vnode
的类型。如果旧vnode
是Fragment
,则需要提供Fragment
自身的实际父级元素作为容器,以便移动其子元素。如果旧vnode
的类型与新vnode
不同,则需要提供正确的父级容器。如果旧vnode
是组件或teleport
,则其子元素可能包含任何内容,因此需要提供正确的父级容器。 - 最后,如果旧
vnode
的父级容器实际上没有使用,则只需将块元素传递给host
函数,以避免调用DOM parentNode
- 先判断旧
- 在
patchElement
函数中,如果新的虚拟节点 n2 上存在动态子节点,就会调用patchBlockChildren
函数对动态子节点进行更新。从patchBlockChildren
的源码可以看到,在对动态子节点进行更新时,实际上是递归调用patch
函数来对动态子节点执行Diff
过程,对动态子节点进行更新。
patchChildren 更新子节点
js
// 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) {
// 先判断子节点是否具有 key。如果子节点中至少有一个 vnode 具有 key,则调用 patchKeyedChildren 函数,对具有 key 的子节点进行更新。
// 接着,传递需要更新的子节点数组、新的子节点数组、容器、锚点、父组件、父 suspense、是否为 SVG、插槽作用域 ID、是否进行优化等参数给 patchKeyedChildren 函数。
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
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 旧子节点是数组时,没有新的子节点,那就卸载旧子节点
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// 旧子节点是文本或者 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
)
}
}
}
}
子节点可以是具有 key
的 vnode
或没有 key
的 vnode
。具有相同 key
的 vnode
被视为相同的 vnode
,可以采用快速更新的方式来更新它们。而没有 key
的 vnode
则需要进行全量更新。
在 patchElement
函数中,如果参数 optimized
的值为 false,即不启用 Diff
优化,那么就会调用 patchChildren
函数对所有子节点执行 diff
过程,对子节点进行更新。在 patchChildren
函数中,开始涉及到 patch
过程中的核心 ------ Diff
算法,这部分内容我们放在下一篇文章中详细解读。
processComponent 处理组件
js
// 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 挂载组件
js
// packages/runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// 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`)
}
// 如果初始化的VNode是 KeepAlive 组件,则在组件实例的上下文中注入 renderer
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// 在初始化组件实例时,需要解析 setup 函数的参数,以便将 props 和 slots 注入到组件实例中。
if (!(__COMPAT__ && compatMountInstance)) {
// 如果是开发环境,则使用 startMeasure 和 endMeasure 函数计时。
if (__DEV__) {
startMeasure(instance, `init`)
}
// 调用 setupComponent 函数,解析 setup 函数的参数并将其注入到组件实例中。
setupComponent(instance)
if (__DEV__) {
endMeasure(instance, `init`)
}
}
if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
// 如果 initialVNode.el 不存在,则需要创建占位符。
// 接着,创建一个 Comment 类型的 vnode 作为占位符,并将其赋值给 instance.subTree。接着,调用 processCommentNode 函数,将占位符 vnode 处理为 Comment 类型的 DOM 节点,并将其插入到容器中。
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
- 因为组件实例的
props
和slots
可以通过setup
函数的参数来访问。在初始化组件实例时,需要解析setup
函数的参数,以便将props
和slots
注入到组件实例中。
- 因为组件实例的
- 最后调用
setupRenderEffect
函数,执行带有副作用的渲染函数。
setupComponent 初始化组件实例
js
// 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
信息返回。
有的同学问了,为什么要区分状态型组件和非状态型组件?
这里就需要了解下状态型组件和非状态型组件的区别了:
- 状态型组件:是指具有响应式状态的组件,即具有
响应式数据、计算属性、侦听器
等。这些组件需要在组件挂载时调用setup
函数来初始化响应式状态,并在组件更新时重新计算响应式状态。为了提高性能,Vue3
会对状态型组件进行优化,以减少不必要的计算和更新。 - 非状态型组件:是指没有响应式状态的组件,即只有静态数据和方法的组件。这些组件不需要调用
setup
函数来初始化响应式状态,只需要在组件挂载时渲染静态内容即可。为了降低内存使用,Vue3
不会为非状态型组件创建额外的响应式状态,从而减少内存占用。
因此,区分状态型组件和非状态型组件可以让 Vue3
针对不同类型的组件进行不同的优化,从而提高性能和降低内存使用。
setupStatefulComponent 生成 setup 信息
js
// 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.`
)
}
}
// 1、创建渲染代理属性访问缓存
instance.accessCache = Object.create(null)
// 2、为组件实例创建渲染代理,同时将代理标记为 raw,
// 为的是在后续过程中不会被误转化为响应式数据,
// 渲染代理源对象是组件实例上下文
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 3、调用 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) {
// 在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.
// 如果开启了 Suspense 特性,则组件在挂载时可以使用 Suspense 组件来等待异步操作完成
// 在组件挂载时,如果 setup 函数返回一个 Promise,则需要等待 Promise 完成后再继续挂载组件
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 {
// 直接将 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
,执行不同的操作。如果是promise
,,则需要等待Promise
完成后再继续挂载组件,执行promise
的 then 函数,获取真正的setup
信息,将其挂载到组件实例上。如果不是promise
,则直接将setup
执行后的结果挂载到组件实例上。
updateComponent 更新组件
js
// 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
) {
// 判断组件是否仍处于异步挂载过程中,并且异步操作仍处于 pending 状态。如果是,则只更新组件的 props 和 slots,不进行渲染。这是因为在组件的 reactive effect for render 还没有设置完成之前,组件无法进行渲染。
if (__DEV__) {
pushWarningContext(n2)
}
// 异步组件,预更新组件
updateComponentPreRender(instance, n2, optimized)
if (__DEV__) {
popWarningContext()
}
return
} else {
// 更新对应组件实例的 next 为新的 VNode
instance.next = n2
// 判断组件实例的 update 函数是否已经被排入队列中。如果是,则使用 invalidateJob 函数将其从队列中移除,以避免重复更新子组件。
invalidateJob(instance.update)
// 触发更新
instance.update()
}
} else {
// 不需要更新,仅将旧虚拟节点上的属性等拷贝到新虚拟节点上
n2.component = n1.component
n2.el = n1.el
instance.vnode = n2
}
}
1、在 updateComponent
函数中,首先从旧虚拟节点 n1
的 component
属性获取当前需要更新的组件实例,并将该组件实例存储到新虚拟节点 n2
的 component
属性上,保持对组件实例的引用。
2、然后根据新旧 vnode
上的 props、指令、子节点等判断是否需要更新组件,如果需要更新组件,则调用组件实例的 update 方法触发更新。这里需要注意组件是否正在挂在中做不同的处理:
- 组件挂载中:在
异步组件
(异步组件是指需要等待异步操作完成后才能渲染的组件)挂载过程中,如果异步操作仍处于pending
状态,则需要暂停组件的挂载过程,并等待异步操作完成后再继续挂载组件。在等待异步操作完成的过程中,可以更新组件的 props 和 slots,但不能进行渲染。 - 组件挂载完成:当一个组件更新时,它的子组件也可能会被更新。如果子组件的更新也被排入队列中,那么在同一刷新中可能会重复更新同一个子组件,导致性能问题。为了避免这种情况,需要在更新子组件之前将其从队列中移除
3、如果不需要更新,仅将旧虚拟节点上的属性等拷贝到新虚拟节点上即可。
Teleport.process 渲染 Teleport 组件
js
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
组件的内容。
Suspense.process 渲染 Suspense 组件
js
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
)
如上面的代码所示,跟 Teleport.process
类似,如果 shapeFlag
的类型为 ShapeFlags.SUSPENSE
,则调用 Suspense
组件内部的 process
函数,渲染 Suspense
组件的内容。
总结
本文主要介绍了 patch
过程中的文本节点、注释节点、静态节点、Fragment节点、Element类型的节点、Component 组件、Teleport 组件、Suspense 异步组件等处理过程。
在调用 patchElement
更新 Element
类型的节点时,会调用 patchChildren
对子节点进行更新,在对子节点进行更新的过程中,需要对新旧子节点进行 Diff 比较(有点多,放到下一节分析)。