已经完成了组件级别的渲染过程分析,组件最终会调用patch函数,而 patch 函数会根据虚拟节点的类型进行不同处理。对于组件节点会递归处理其子组件,对于普通元素节点则会调用 processElement 函数进行处理。
这种递归调用形成了一个树形的渲染结构:从根组件开始,通过 patch 函数逐层向下处理,直到遇到普通元素节点。此时就从组件级别的渲染转换为 DOM 元素级别的渲染,通过 processElement 函数实现。
processElement函数解析
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
if (n2.type === 'svg') {
namespace = 'svg'
} else if (n2.type === 'math') {
namespace = 'mathml'
}
// 2. 根据是否存在旧节点选择挂载或更新
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
mountElement详细分析
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { props, shapeFlag, transition, dirs } = vnode
el = vnode.el = hostCreateElement(
vnode.type as string,
namespace,
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) {
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
resolveChildrenNamespace(vnode, namespace),
slotScopeIds,
optimized,
)
}
// 3. 处理指令
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// scopeId
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
// 5. 处理props
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
hostPatchProp(el, key, null, props[key], namespace, parentComponent)
}
}
if ('value' in props) {
hostPatchProp(el, 'value', null, props.value, namespace)
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
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
const needCallTransitionHooks = needTransition(parentSuspense, transition)
if (needCallTransitionHooks) {
transition!.beforeEnter(el)
}
hostInsert(el, container, anchor)
if (
(vnodeHook = props && props.onVnodeMounted) ||
needCallTransitionHooks ||
dirs
) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
}
patchElement详细分析
Vue提供了两种子节点更新策略:
-
Block树更新(优化模式):
- 只更新动态子节点
- 跳过静态内容
- 性能更好,适用于编译优化的情况
-
完整的子节点diff:
- 对所有子节点进行比较
- 使用传统的diff算法
- 更通用,但性能较差
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
// 1. 复用DOM元素
const el = (n2.el = n1.el!)
// 2. 获取新旧props和其他需要的信息
let { patchFlag, dynamicChildren, dirs } = n2
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
// 3. 调用beforeUpdate钩子
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
// 4. 处理子节点更新
if (dynamicChildren) {
// 优化模式:只更新动态子节点
patchBlockChildren(
n1.dynamicChildren!,
dynamicChildren,
el,
parentComponent,
parentSuspense,
resolveChildrenNamespace(n2, namespace),
slotScopeIds,
)
} else if (!optimized) {
// 完整diff
patchChildren(
n1,
n2,
el,
null,
parentComponent,
parentSuspense,
resolveChildrenNamespace(n2, namespace),
slotScopeIds,
false,
)
}
// 5. 根据patchFlag优化更新props
if (patchFlag > 0) {
if (patchFlag & PatchFlags.FULL_PROPS) {
// 完整props更新
patchProps(el, oldProps, newProps, parentComponent, namespace)
} else {
// 按需更新class
if (patchFlag & PatchFlags.CLASS) {
if (oldProps.class !== newProps.class) {
hostPatchProp(el, 'class', null, newProps.class, namespace)
}
}
// 按需更新style
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', oldProps.style, newProps.style, namespace)
}
// 按需更新动态props
if (patchFlag & PatchFlags.PROPS) {
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
if (next !== prev || key === 'value') {
hostPatchProp(el, key, prev, next, namespace, parentComponent)
}
}
}
}
}
// 6. 调用updated钩子
if ((dirs)) {
queuePostRenderEffect(() => {
dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
}, parentSuspense)
}
}
patchChildren函数分析
const patchBlockChildren = (
oldChildren,
newChildren,
fallbackContainer,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
slotScopeIds,
) => {
for (let i = 0; i < newChildren.length; i++) {
const oldVNode = oldChildren[i]
const newVNode = newChildren[i]
// 确定更新的容器
const container =
oldVNode.el &&
(oldVNode.type === Fragment ||
!isSameVNodeType(oldVNode, newVNode) ||
oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
? hostParentNode(oldVNode.el)!
: fallbackContainer
// 对每个节点调用patch进行更新
patch(
oldVNode,
newVNode,
container,
null,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
true,
)
}
}
核心设计
-
优化更新范围
-
块树优化(Block Tree):
- 在编译阶段,Vue会将模板编译为渲染函数
- 编译器会标记出所有动态节点,收集到Block中
- 这些动态节点会形成一个扁平化的数组,称为"dynamicChildren"
- Block树中只有动态节点会被追踪,静态节点会被完全跳过
-
动态节点收集:
-
编译器会识别模板中的动态绑定,如:
- 动态属性:v-bind、:
- 动态文本:{{ }}
- 动态指令:v-if、v-for等
-
这些动态节点会被赋予不同的 PatchFlag,用于标记其动态特性
-
PatchFlag 会指示运行时如何更新这个节点
-
-
更新优化:
- patchBlockChildren 只处理 dynamicChildren 数组中的节点
- 由于数组是扁平的,不需要递归遍历整个树结构
- 静态节点完全不会参与 diff 过程
- 动态节点可以直接一一对应更新,因为它们的顺序是稳定的
-
-
容器确定策略
-
fallbackContainer 是更新操作的默认容器,通常是当前正在处理的DOM元素
-
在以下三种情况下,需要获取真实的父容器(hostParentNode)而不是使用 fallbackContainer:
- Fragment 类型:因为 Fragment 本身不会渲染成真实DOM,需要获取实际的父容器
- 新旧节点类型不同:需要在实际的父容器中完成替换操作
- 组件或传送门:这些特殊节点可能会改变DOM结构,需要确保在正确的容器中更新
-
使用 fallbackContainer 的情况:
- 当节点类型相同且不是特殊节点时
- 这种情况下可以直接在当前容器中更新,无需获取父节点
- 这是一种优化手段,避免不必要的 DOM 父节点查找操作
-
-
更新方式
- 直接调用patch
- 保持节点顺序
- 一对一更新,无需diff
patchChildren实现分析
const patchChildren = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace: ElementNamespace,
slotScopeIds,
optimized = false,
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// 快速路径处理
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// 处理带key的片段
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// 处理无key的片段
patchUnkeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
return
}
}
// 处理三种可能的情况:文本、数组或无子节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 文本子节点的快速路径
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 之前的子节点是数组
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 两个数组,需要完整的diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
// 没有新的子节点,卸载旧的
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// 之前的子节点是文本或null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// 挂载新的数组子节点
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
}
}
更新策略分析
-
PatchFlag优化
- KEYED_FRAGMENT:带key的片段更新
- UNKEYED_FRAGMENT:无key的片段更新
- 利用编译时信息优化运行时性能
-
子节点类型处理
- 文本子节点:直接替换
- 数组子节点:需要diff算法
- 无子节点:直接清空
-
不同场景的优化
- 数组 -> 数组:完整diff
- 数组 -> 文本:卸载后设置文本
- 文本 -> 数组:清空后挂载
- 文本 -> 文本:直接替换
总结
通过分析这两个函数,我们可以看到Vue在DOM更新时采用了多层次的优化策略:
-
Block树优化
- 编译时收集动态节点到 dynamicChildren 数组
- 扁平化的动态节点数组,避免树形递归
- 静态节点完全跳过,不参与更新过程
-
更新类型优化
- 基于 PatchFlag 的快速路径处理
- 针对性处理 KEYED_FRAGMENT 和 UNKEYED_FRAGMENT
- 区分文本节点和数组节点的更新策略
-
DOM操作优化
- 复用 DOM 节点,避免不必要的创建和销毁
- 优化容器查找策略,减少 DOM 父节点查找
- 根据节点类型选择最优的更新路径
这些优化策略让Vue能够在保证功能的同时,最小化DOM操作次数,提供高效的更新性能。在下一篇文章中,我们将深入分析 patchKeyedChildren 函数和patchUnkeyedChildren函数,了解Vue的核心diff算法实现。
patchUnkeyedChildren实现分析
const patchUnkeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length
const newLength = c2.length
const commonLength = Math.min(oldLength, newLength)
let i
for (i = 0; i < commonLength; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
if (oldLength > newLength) {
// 移除多余的旧节点
unmountChildren(
c1,
parentComponent,
parentSuspense,
true,
false,
commonLength,
)
} else {
// 挂载新增的节点
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
commonLength,
)
}
}
patchKeyedChildren实现分析
const patchKeyedChildren = (
c1: VNode[],
c2: VNodeArrayChildren,
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
let i = 0
const l2 = c2.length
let e1 = c1.length - 1 // 旧节点的结束索引
let e2 = l2 - 1 // 新节点的结束索引
// 1. 从头部开始同步
// (a b) c
// (a b) d e
// 这里的 a b 是相同的前缀节点,可以直接复用
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, null, parentComponent, parentSuspense,
namespace, slotScopeIds, optimized)
} else {
break
}
i++
}
// 2. 从尾部开始同步
// a (b c)
// d e (b c)
// 这里的 b c 是相同的后缀节点,可以直接复用
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, null, parentComponent, parentSuspense,
namespace, slotScopeIds, optimized)
} else {
break
}
e1--
e2--
}
// 3. 处理新增节点
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// 这种情况下需要新增节点 c
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch(null, c2[i], container, anchor, parentComponent,
parentSuspense, namespace, slotScopeIds, optimized)
i++
}
}
}
// 4. 处理需要删除的节点
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// 这种情况下需要删除节点 c
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 5. 处理未知序列
// 例如,有如下更新:
// 旧子节点: [p q] [a b c d e] [x y]
// 新子节点: [p q] [c e b a d f] [x y]
// 经过前面的首尾处理后:
// - 先同步了前缀 [p q]
// - 再同步了后缀 [x y]
// - 剩余中间部分 [a b c d e] -> [c e b a d f] 需要处理
else {
const s1 = i
const s2 = i
// 5.1 建立新节点的key到索引的映射
// 为剩余的 [c e b a d f] 建立映射:
// {
// c: 0,
// e: 1,
// b: 2,
// a: 3,
// d: 4,
// f: 5
// }
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 5.2 遍历旧节点,尝试patch匹配的节点并移除不再存在的节点
// 遍历剩余的旧子节点 [a b c d e],尝试在新子节点中找到对应位置
// newIndexToOldIndexMap 记录新子节点在旧子节点中的位置
// 对于 [a b c d e] -> [c e b a d f] 的映射关系是:
// newIndexToOldIndexMap: [3, 5, 2, 1, 4, 0]
// 这个数组表示:
// 索引: 0 1 2 3 4 5
// 新节点: c e b a d f
// 旧位置: 3 5 2 1 4 0 (注:实际值都+1了,0表示新节点)
let j
let patched = 0
const toBePatched = e2 - s2 + 1 // 需要处理的新节点数量
let moved = false
let maxNewIndexSoFar = 0
const newIndexToOldIndexMap = new Array(toBePatched)
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
if (patched >= toBePatched) {
// 所有新节点都已patch,剩余的旧节点可以直接删除
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
// 通过key快速找到新的位置
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// 没有key只能遍历查找
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
if (newIndex === undefined) {
// 在新子节点中找不到,需要删除
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 记录新节点在旧节点中的位置
newIndexToOldIndexMap[newIndex - s2] = i + 1
// 用于判断是否需要移动
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
// 更新节点
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
patched++
}
}
// 5.3 移动和挂载
// 最长递增子序列是 [0, 4],对应新节点序列中的c和d
// 这意味着这两个节点可以保持相对位置不变,其他节点需要移动
//
// Vue从后往前遍历处理节点:
// 1. f是新节点,直接创建并插入
// 2. d在最长递增子序列中,保持不动
// 3. a需要移动到d之前
// 4. b需要移动到a之前
// 5. e需要移动到b之前
// 6. c在最长递增子序列中,保持不动
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap) // 获取最长递增子序列
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
// 从后向前遍历,这样可以使用后面的节点作为锚点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
// 确定锚点(下一个节点的DOM元素)
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// 新节点,需要挂载
// 例如例子中的节点f
patch(null, nextChild, container, anchor, parentComponent,
parentSuspense, namespace, slotScopeIds, optimized)
} else if (moved) {
// 需要移动的节点
// 如果没有最长递增子序列,或当前节点不在最长递增子序列中
// 例如例子中的节点e、b、a需要移动
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
// 当前节点在最长递增子序列中,不需要移动
// 例如例子中的节点c和d
j--
}
}
}
}
}
diff过程采用的方法
const move: MoveFn = (
vnode,
container,
anchor,
moveType,
parentSuspense = null,
) => {
const { el, type, transition, children, shapeFlag } = vnode
if (shapeFlag & ShapeFlags.COMPONENT) {
move(vnode.component!.subTree, container, anchor, moveType)
return
}
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
vnode.suspense!.move(container, anchor, moveType)
return
}
if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).move(vnode, container, anchor, internals)
return
}
if (type === Fragment) {
hostInsert(el!, container, anchor)
for (let i = 0; i < (children as VNode[]).length; i++) {
move((children as VNode[])[i], container, anchor, moveType)
}
hostInsert(vnode.anchor!, container, anchor)
return
}
if (type === Static) {
moveStaticNode(vnode, container, anchor)
return
}
// single nodes
const needTransition =
moveType !== MoveType.REORDER &&
shapeFlag & ShapeFlags.ELEMENT &&
transition
if (needTransition) {
if (moveType === MoveType.ENTER) {
transition!.beforeEnter(el!)
hostInsert(el!, container, anchor)
queuePostRenderEffect(() => transition!.enter(el!), parentSuspense)
} else {
const { leave, delayLeave, afterLeave } = transition!
const remove = () => hostInsert(el!, container, anchor)
const performLeave = () => {
leave(el!, () => {
remove()
afterLeave && afterLeave()
})
}
if (delayLeave) {
delayLeave(el!, remove, performLeave)
} else {
performLeave()
}
}
} else {
hostInsert(el!, container, anchor)
}
}
const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false,
) => {
const {
type,
props,
ref,
children,
dynamicChildren,
shapeFlag,
patchFlag,
dirs,
cacheIndex,
} = vnode
if (patchFlag === PatchFlags.BAIL) {
optimized = false
}
// unset ref
if (ref != null) {
setRef(ref, null, parentSuspense, vnode, true)
}
// #6593 should clean memo cache when unmount
if (cacheIndex != null) {
parentComponent!.renderCache[cacheIndex] = undefined
}
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
return
}
const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs
const shouldInvokeVnodeHook = !isAsyncWrapper(vnode)
let vnodeHook: VNodeHook | undefined | null
if (
shouldInvokeVnodeHook &&
(vnodeHook = props && props.onVnodeBeforeUnmount)
) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
if (shapeFlag & ShapeFlags.COMPONENT) {
unmountComponent(vnode.component!, parentSuspense, doRemove)
} else {
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
vnode.suspense!.unmount(parentSuspense, doRemove)
return
}
if (shouldInvokeDirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount')
}
if (shapeFlag & ShapeFlags.TELEPORT) {
;(vnode.type as typeof TeleportImpl).remove(
vnode,
parentComponent,
parentSuspense,
internals,
doRemove,
)
} else if (
dynamicChildren &&
// #5154
// when v-once is used inside a block, setBlockTracking(-1) marks the
// parent block with hasOnce: true
// so that it doesn't take the fast path during unmount - otherwise
// components nested in v-once are never unmounted.
!dynamicChildren.hasOnce &&
// #1153: fast path should not be taken for non-stable (v-for) fragments
(type !== Fragment ||
(patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
) {
// fast path for block nodes: only need to unmount dynamic children.
unmountChildren(
dynamicChildren,
parentComponent,
parentSuspense,
false,
true,
)
} else if (
(type === Fragment &&
patchFlag &
(PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) ||
(!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN)
) {
unmountChildren(children as VNode[], parentComponent, parentSuspense)
}
if (doRemove) {
remove(vnode)
}
}
if (
(shouldInvokeVnodeHook &&
(vnodeHook = props && props.onVnodeUnmounted)) ||
shouldInvokeDirs
) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
shouldInvokeDirs &&
invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
}, parentSuspense)
}
}
const remove: RemoveFn = vnode => {
const { type, el, anchor, transition } = vnode
if (type === Fragment) {
removeFragment(el!, anchor!)
return
}
if (type === Static) {
removeStaticNode(vnode)
return
}
const performRemove = () => {
hostRemove(el!)
if (transition && !transition.persisted && transition.afterLeave) {
transition.afterLeave()
}
}
if (
vnode.shapeFlag & ShapeFlags.ELEMENT &&
transition &&
!transition.persisted
) {
const { leave, delayLeave } = transition
const performLeave = () => leave(el!, performRemove)
if (delayLeave) {
delayLeave(vnode.el!, performRemove, performLeave)
} else {
performLeave()
}
} else {
performRemove()
}
}
const removeFragment = (cur: RendererNode, end: RendererNode) => {
// For fragments, directly remove all contained DOM nodes.
// (fragment child nodes cannot have transition)
let next
while (cur !== end) {
next = hostNextSibling(cur)!
hostRemove(cur)
cur = next
}
hostRemove(end)
}
const unmountComponent = (
instance: ComponentInternalInstance,
parentSuspense: SuspenseBoundary | null,
doRemove?: boolean,
) => {
const { bum, scope, update, subTree, um, m, a } = instance
invalidateMount(m)
invalidateMount(a)
// beforeUnmount hook
if (bum) {
invokeArrayFns(bum)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
instance.emit('hook:beforeDestroy')
}
// stop effects in component scope
scope.stop()
// update may be null if a component is unmounted before its async
// setup has resolved.
if (update) {
// so that scheduler will no longer invoke it
update.active = false
unmount(subTree, instance, parentSuspense, doRemove)
}
// unmounted hook
if (um) {
queuePostRenderEffect(um, parentSuspense)
}
if (
__COMPAT__ &&
isCompatEnabled(DeprecationTypes.INSTANCE_EVENT_HOOKS, instance)
) {
queuePostRenderEffect(
() => instance.emit('hook:destroyed'),
parentSuspense,
)
}
queuePostRenderEffect(() => {
instance.isUnmounted = true
}, parentSuspense)
// A component with async dep inside a pending suspense is unmounted before
// its async dep resolves. This should remove the dep from the suspense, and
// cause the suspense to resolve immediately if that was the last dep.
if (
__FEATURE_SUSPENSE__ &&
parentSuspense &&
parentSuspense.pendingBranch &&
!parentSuspense.isUnmounted &&
instance.asyncDep &&
!instance.asyncResolved &&
instance.suspenseId === parentSuspense.pendingId
) {
parentSuspense.deps--
if (parentSuspense.deps === 0) {
parentSuspense.resolve()
}
}
}
const unmountChildren: UnmountChildrenFn = (
children,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false,
start = 0,
) => {
for (let i = start; i < children.length; i++) {
unmount(children[i], parentComponent, parentSuspense, doRemove, optimized)
}
}
总结
通过分析这两个函数,我们可以看到Vue在处理子节点更新时采用了不同的策略:
-
无key子节点
- 简单的位置对应更新
- 适用于静态或很少变化的列表
- 性能较差,不建议在动态列表中使用
-
带key子节点
- 复杂但高效的diff算法
- 优化的节点复用策略
- 最小化DOM操作
-
性能优化
- 多级别的优化策略
- 智能的节点复用
- 最优的DOM操作序列
这种分层的更新策略让Vue能够在不同场景下都能提供最优的更新性能。在实际开发中,我们应该:
- 总是为动态列表提供key
- 避免使用索引作为key
- 优先考虑稳定且唯一的值作为key