Appearance
第2.2节:完整的 DOM Diff 流程是怎样的?(上)
在上一节中,我们了解了 VNode 到 DOM 的转换过程。本节将深入探讨 Vue 3 中最核心的机制之一:DOM Diff 算法。当响应式数据发生变化时,Vue 如何高效地更新 DOM?这个过程涉及响应式触发、调度系统、组件更新和渲染优化等多个环节。
1. 更新机制概览
1.1 从数据变化到 DOM 更新的完整链路
数据变化 → 响应式触发 → 调度器入队 → 异步更新 → 组件重渲染 → DOM Diff → DOM 更新让我们通过一个简单的例子来理解这个过程:
javascript
// 当我们修改响应式数据时
const state = reactive({ count: 0 })
state.count++ // 触发更新链路1.2 核心源码文件结构
- - 渲染器核心,包含 patch、diff 算法
- - 调度系统,管理更新队列
- - 组件渲染工具函数
2. 响应式触发:数据变化到更新队列
2.1 响应式系统的触发机制
当响应式数据发生变化时,会触发依赖收集的副作用函数。在组件中,这个副作用函数就是组件的更新函数:
typescript
// 在 setupRenderEffect 中创建的 ReactiveEffect
const effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update), // scheduler
instance.scope
)2.2 组件更新函数的调度
函数中定义了组件的更新逻辑:typescript
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 初始挂载逻辑
// ...
} else {
// 更新逻辑
let { next, bu, u, parent, vnode } = instance
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
// beforeUpdate 钩子
if (bu) {
invokeArrayFns(bu)
}
// 重新渲染组件
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
// 执行 patch 进行 diff
patch(
prevTree,
nextTree,
hostParentNode(prevTree.el!)!,
getNextHostNode(prevTree),
instance,
parentSuspense,
namespace
)
}
}3. 调度系统:nextTick 和异步更新
3.1 更新队列管理
Vue 3 的调度系统通过 函数管理更新队列:
typescript
export function queueJob(job: SchedulerJob) {
// 避免重复入队
if (
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
)
) {
if (job.id == null) {
queue.push(job)
} else {
// 根据 id 插入到正确位置,保证父组件先于子组件更新
queue.splice(findInsertionIndex(job.id), 0, job)
}
queueFlush()
}
}3.2 异步刷新机制
确保更新在下一个微任务中执行:typescript
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}3.3 任务执行:flushJobs
负责执行队列中的所有更新任务:typescript
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
// 排序确保父组件先于子组件更新
queue.sort(comparator)
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
// 执行后置回调
flushPostFlushCbs(seen)
isFlushing = false
currentFlushPromise = null
// 递归处理新产生的任务
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
}4. 组件更新:updateComponent 流程
4.1 组件更新的触发条件
函数决定组件是否需要更新:typescript
export function shouldUpdateComponent(
prevVNode: VNode,
nextVNode: VNode,
optimized?: boolean,
): boolean {
const { props: prevProps, children: prevChildren, component } = prevVNode
const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
// 强制更新
if (patchFlag & PatchFlags.FORCE_UPDATE) {
return true
}
// 优化模式下的快速路径
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
return hasPropsChanged(prevProps, nextProps, component!.emitsOptions)
}
// 检查 props 变化
if (prevProps === nextProps) {
return !!(prevChildren || nextChildren)
}
return hasPropsChanged(prevProps, nextProps, component!.emitsOptions)
}4.2 组件实例的更新生命周期
函数处理组件更新:typescript
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, optimized)) {
instance.next = n2
// 移除已排队的更新任务,避免重复更新
invalidateJob(instance.update)
// 触发组件更新
instance.update()
} else {
// 无需更新,直接复用
n2.el = n1.el
instance.vnode = n2
}
}5. 子节点的 Diff 算法
5.1 patchChildren:子节点更新的入口
函数根据子节点类型选择不同的更新策略:typescript
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized = false,
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { patchFlag, shapeFlag } = n2
// 快速路径:处理 patchFlag
if (patchFlag > 0) {
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// 有 key 的片段,使用完整 diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// 无 key 的片段,简单 diff
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 {
// 旧子节点是文本或空
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 挂载新的数组子节点
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
}
}5.2 patchKeyedChildren:核心 Diff 算法
是 Vue 3 最核心的 diff 算法,采用了双端比较 + 最长递增子序列的优化策略:typescript
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
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)
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
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] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
i++
}
}
}
// 4. 公共序列 + 卸载
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 5. 未知序列
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
// 这里是最复杂的情况,需要使用最长递增子序列算法
// 详细实现见下一节
}
}6. 渲染优化:静态提升和 Patch Flags
6.1 Patch Flags 优化
Vue 3 编译器会为 VNode 添加 patchFlag,标记节点的动态部分:
typescript
export const enum PatchFlags {
TEXT = 1, // 动态文本内容
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态属性
FULL_PROPS = 1 << 4, // 具有动态 key 的属性
HYDRATE_EVENTS = 1 << 5, // 事件监听器
STABLE_FRAGMENT = 1 << 6, // 稳定的片段
KEYED_FRAGMENT = 1 << 7, // 有 key 的片段
UNKEYED_FRAGMENT = 1 << 8, // 无 key 的片段
NEED_PATCH = 1 << 9, // 需要 patch
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
DEV_ROOT_FRAGMENT = 1 << 11, // 开发模式根片段
HOISTED = -1, // 静态提升
BAIL = -2, // diff 算法回退
}6.2 静态提升优化
编译器会将静态节点提升到渲染函数外部,避免重复创建:
javascript
// 编译前
<template>
<div>
<h1>静态标题</h1>
<p>{{ message }}</p>
</div>
</template>
// 编译后(简化)
const _hoisted_1 = createElementVNode("h1", null, "静态标题")
function render() {
return createElementVNode("div", null, [
_hoisted_1, // 静态节点被提升
createElementVNode("p", null, toDisplayString(message), 1 /* TEXT */)
])
}7. 父子组件更新顺序
7.1 更新队列的排序机制
Vue 3 通过组件的 id 确保父组件先于子组件更新:
typescript
const getId = (job: SchedulerJob): number =>
job.id == null ? Infinity : job.id
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
const diff = getId(a) - getId(b)
if (diff === 0) {
if (a.pre && !b.pre) return -1
if (b.pre && !a.pre) return 1
}
return diff
}7.2 组件 ID 的分配
组件实例创建时会分配递增的 ID:
typescript
let uid = 0
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null,
) {
const instance: ComponentInternalInstance = {
uid: uid++, // 递增的唯一 ID
vnode,
type: vnode.type,
parent,
// ...
}
return instance
}小结
本节我们深入分析了 Vue 3 DOM Diff 流程的前半部分,包括:
- 响应式触发机制:从数据变化到更新队列的完整链路
- 调度系统:异步更新和任务队列管理
- 组件更新流程:shouldUpdateComponent 和 updateComponent 的工作原理
- 子节点 Diff:patchChildren 和 patchKeyedChildren 的基本策略
- 渲染优化:Patch Flags 和静态提升的优化机制
- 更新顺序:父子组件的更新顺序保证
在下一节中,我们将继续深入探讨 patchKeyedChildren 中最复杂的"未知序列"处理逻辑,以及最长递增子序列算法的具体实现和优化原理。
