Appearance
第 11 章 虚拟 DOM 与 Diff 算法
本章要点
- 虚拟 DOM 的本质:从 VNode 的数据结构到它如何成为 UI 的中间表示层
- VNode 的类型系统:ShapeFlag 位掩码如何用一个整数编码所有节点类型信息
- patch 函数的分发逻辑:如何根据 VNode 类型选择不同的处理路径
- 子节点 Diff 的核心算法:双端比较、最长递增子序列、key 的关键作用
- Block Tree 与 PatchFlag:编译时优化如何让 Diff 从 O(n) 降到 O(动态节点数)
- Fragment、Teleport、Suspense 等特殊 VNode 的处理策略
- Vue 3.6 Vapor Mode 对传统 VNode 体系的挑战与共存
在前面的章节中,我们已经深入了编译器和组件系统。编译器将模板转化为渲染函数,组件系统管理每个组件的生命和状态。但在这两者之间,还有一个至关重要的中间层——虚拟 DOM。
当渲染函数执行时,它不会直接操作真实 DOM,而是生成一棵由 JavaScript 对象构成的虚拟节点树。当组件状态变化时,新的虚拟树与旧的虚拟树进行对比——这个过程就是 Diff。Diff 的结果是一组最小化的 DOM 操作指令,精确地把界面从旧状态更新到新状态。
这一章,我们要完全拆解这个过程。
11.1 VNode:虚拟 DOM 的原子
VNode 的数据结构
每一个虚拟节点都是一个 VNode 对象。它的结构远比"一个标签名加一堆属性"复杂得多:
typescript
// packages/runtime-core/src/vnode.ts
export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = { [key: string]: any }
> {
__v_isVNode: true // VNode 标记
type: VNodeTypes // 节点类型:string | Component | Fragment | ...
props: (VNodeProps & ExtraProps) | null
key: string | number | symbol | null // Diff 的身份标识
ref: VNodeNormalizedRef | null // 模板 ref
children: VNodeNormalizedChildren // 子节点
// ---- 运行时状态 ----
el: HostNode | null // 对应的真实 DOM 节点
anchor: HostNode | null // Fragment 的锚点
component: ComponentInternalInstance | null // 组件实例引用
// ---- 优化标记 ----
shapeFlag: number // 节点形状位掩码
patchFlag: number // 编译器标记的动态类型
dynamicProps: string[] | null // 动态属性名列表
dynamicChildren: VNode[] | null // Block 收集的动态子节点
// ---- 其他 ----
dirs: DirectiveBinding[] | null // 指令绑定
transition: TransitionHooks | null
suspense: SuspenseBoundary | null
appContext: AppContext | null
}一个 VNode 既是 UI 的描述,又携带了优化信息,还反向引用了真实 DOM。它是连接"声明式意图"和"命令式操作"的桥梁。
ShapeFlag:用位运算编码节点类型
Vue 使用一个整数的不同位来标记 VNode 的类型信息,这是一种经典的位掩码模式:
typescript
// packages/shared/src/shapeFlags.ts
export enum ShapeFlags {
ELEMENT = 1, // 0000 0001 — 普通 HTML 元素
FUNCTIONAL_COMPONENT = 1 << 1, // 0000 0010 — 函数式组件
STATEFUL_COMPONENT = 1 << 2, // 0000 0100 — 有状态组件
TEXT_CHILDREN = 1 << 3, // 0000 1000 — 子节点是文本
ARRAY_CHILDREN = 1 << 4, // 0001 0000 — 子节点是数组
SLOTS_CHILDREN = 1 << 5, // 0010 0000 — 子节点是插槽
TELEPORT = 1 << 6, // 0100 0000 — Teleport 组件
SUSPENSE = 1 << 7, // 1000 0000 — Suspense 组件
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
COMPONENT_KEPT_ALIVE = 1 << 9,
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}为什么用位掩码而不是字符串枚举?因为位运算的判断只需要一条 CPU 指令:
typescript
// 判断是否是元素
if (shapeFlag & ShapeFlags.ELEMENT) { /* ... */ }
// 判断是否是组件且有数组子节点
if (shapeFlag & ShapeFlags.COMPONENT && shapeFlag & ShapeFlags.ARRAY_CHILDREN) { /* ... */ }
// 创建时组合标记
const shapeFlag = ShapeFlags.ELEMENT | ShapeFlags.ARRAY_CHILDREN在 Diff 的热路径上,每一个 if 判断都被执行成千上万次。位运算比字符串比较快一个数量级,这是框架级性能优化的典型手法。
createVNode:VNode 的工厂
渲染函数中的 h() 最终调用 createVNode 来创建 VNode:
typescript
// packages/runtime-core/src/vnode.ts(简化)
export function createVNode(
type: VNodeTypes,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false
): VNode {
// 1. 规范化 type
if (isVNode(type)) {
// 克隆已有 VNode
return cloneVNode(type, props)
}
// 2. 类组件规范化
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 3. 规范化 props(class、style 合并)
if (props) {
props = guardReactiveProps(props)
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
props.style = normalizeStyle(style)
}
}
// 4. 计算 shapeFlag
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
// 5. 创建 VNode 对象
return createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode)
}注意 patchFlag 和 dynamicProps 参数——它们由编译器注入,运行时不会自己去分析哪些属性是动态的。这就是 Vue 3 的编译-运行时协作模型。
11.2 patch:万物的入口
patch 函数的分发逻辑
patch 是整个渲染器的核心入口。无论是首次渲染还是更新,都从 patch 开始:
typescript
// packages/runtime-core/src/renderer.ts(简化)
const patch: PatchFn = (
n1, // 旧 VNode(null 表示首次挂载)
n2, // 新 VNode
container, // DOM 容器
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
slotScopeIds = null,
optimized = false
) => {
// 1. 如果新旧节点类型完全不同,直接卸载旧的
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null // 重置为 null,后续走挂载逻辑
}
// 2. 如果新节点标记为 BAIL,关闭优化
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
// 3. 根据类型分发
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, namespace)
}
break
case Fragment:
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals)
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(n1, n2, container, anchor, parentComponent, parentSuspense, namespace, slotScopeIds, optimized, internals)
}
}
// 4. 设置 ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}