Vue 节点类型处理机制分析
前言
上一节,我们分析到了patch函数(diff算法)的主要执行流程,本节我们来具体分析不同类型节点的diff算法。
节点类型概览
Vue 中的节点类型可以分为以下几类:
-
基础节点类型
- Text: 文本节点
- Comment: 注释节点
- Static: 静态节点
- Fragment: 片段节点
-
复杂节点类型
- Element: 普通元素节点
- Component: 组件节点
- Teleport: 传送门组件
- Suspense: 异步组件
各类型节点处理机制
1. 基础节点处理
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);
} else if (__DEV__) {
patchStaticNode(n1, n2, container, namespace);
}
break;
case Fragment:
processFragment(/*...参数...*/);
break;
}
Text 节点
源码实现
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
// 首次挂载:创建文本节点并插入
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
);
} else {
// 更新:复用旧节点,只更新文本内容
const el = (n2.el = n1.el!);
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string);
}
}
};
使用示例
<!-- 直接文本 -->
Hello World
<!-- 动态文本 -->
{{ message }}
Comment 节点
源码实现
const processCommentNode: ProcessTextOrCommentFn = (
n1,
n2,
container,
anchor
) => {
if (n1 == null) {
// 首次挂载:创建并插入注释节点
hostInsert(
(n2.el = hostCreateComment((n2.children as string) || "")),
container,
anchor
);
} else {
// 更新:注释节点不支持动态更新,直接复用
n2.el = n1.el;
}
};
使用示例
<!-- 这是一个注释 -->
<!--
多行注释
用于调试或文档说明
-->
特殊处理说明
- 注释节点不支持动态更新
- 在开发环境下保留,生产环境可能被移除
- 用于调试和文档目的
Static 节点
源码实现
// 挂载静态节点
const mountStaticNode = (
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
namespace: ElementNamespace
) => {
// 静态节点仅在使用 compiler-dom/runtime-dom 时存在
[n2.el, n2.anchor] = hostInsertStaticContent!(
n2.children as string,
container,
anchor,
namespace,
n2.el,
n2.anchor
);
};
// 更新静态节点(仅开发环境)
const patchStaticNode = (
n1: VNode,
n2: VNode,
container: RendererElement,
namespace: ElementNamespace
) => {
// 仅在开发环境下为了 HMR 而更新静态节点
if (n2.children !== n1.children) {
const anchor = hostNextSibling(n1.anchor!);
// 移除旧的
removeStaticNode(n1)[
// 插入新的
(n2.el, n2.anchor)
] = hostInsertStaticContent!(
n2.children as string,
container,
anchor,
namespace
);
} else {
// 无变化时复用节点
n2.el = n1.el;
n2.anchor = n1.anchor;
}
};
// 移动静态节点
const moveStaticNode = (
{ el, anchor }: VNode,
container: RendererElement,
nextSibling: RendererNode | null
) => {
let next;
while (el && el !== anchor) {
next = hostNextSibling(el);
hostInsert(el, container, nextSibling);
el = next;
}
hostInsert(anchor!, container, nextSibling);
};
// 移除静态节点
const removeStaticNode = ({ el, anchor }: VNode) => {
let next;
while (el && el !== anchor) {
next = hostNextSibling(el);
hostRemove(el);
el = next;
}
hostRemove(anchor!);
};
使用示例
<!-- 编译时优化的静态内容 -->
<div class="static">
<h1>静态标题</h1>
<p>静态段落</p>
</div>
<!-- 带有静态树的模板 -->
<template>
<div>
<header>
<!-- 这部分在编译时被标记为静态 -->
<logo />
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
</nav>
</header>
<!-- 动态内容 -->
<main>{{ content }}</main>
</div>
</template>
特殊处理说明
-
优化机制
- 编译时标记静态内容
- 运行时跳过 diff
- 仅在开发环境支持更新(HMR)
-
操作特点
- 使用锚点标记范围
- 支持整体移动
- 批量删除优化
Fragment 节点
源码实现
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null
/* ...其他参数... */
) => {
// 创建或复用首尾锚点
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(""))!;
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(""))!;
if (n1 == null) {
// 首次挂载:插入锚点并挂载子节点
hostInsert(fragmentStartAnchor, container, anchor);
hostInsert(fragmentEndAnchor, container, anchor);
mountChildren(
(n2.children || []) as VNodeArrayChildren,
container,
fragmentEndAnchor
/* ...其他参数... */
);
} else {
// 更新:根据情况选择更新策略
if (patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT) {
// 稳定片段优化更新
patchBlockChildren(/*...参数...*/);
} else {
// 完整子节点更新
patchChildren(/*...参数...*/);
}
}
};
使用示例
<!-- 多根节点模板 -->
<template>
<div>First</div>
<div>Second</div>
</template>
<!-- v-for 片段 -->
<template>
<div v-for="item in items">{{ item }}</div>
</template>
<!-- 结构固定,仅内容变化 -->
<template>
<div>固定的</div>
<div>{{ dynamic }}</div>
</template>
特殊处理说明
- 使用空文本节点作为锚点标记范围,便于定位和移动片段内容
- 支持优化更新(STABLE_FRAGMENT):
- 通过 patchFlag 标记稳定片段
- 仅更新动态子节点(dynamicChildren)
- 跳过静态内容的 diff
- 减少不必要的 DOM 操作
2. 复杂节点处理
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(/*...参数...*/);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(/*...参数...*/);
} else if (shapeFlag & ShapeFlags.TELEPORT) {
type.process(/*...参数...*/);
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
type.process(/*...参数...*/);
}
Element 节点
源码实现
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
) => {
// 1. 处理命名空间
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
);
}
};
特殊处理说明
-
命名空间处理
- SVG 元素特殊处理
- MathML 支持
- 命名空间继承
-
挂载和更新策略
- 首次挂载:创建新元素
- 更新:复用并 patch
- 优化处理
Component 节点
源码实现
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean
) => {
// 1. 继承slot作用域id
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
// 2. 挂载组件
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
// keepAlive 激活
(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
namespace,
optimized
);
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
}
} else {
// 3. 更新组件
updateComponent(n1, n2, optimized);
}
};
特殊处理说明
-
组件生命周期管理
- 组件的创建、挂载和更新过程
- 通过 mountComponent 和 updateComponent 实现
-
特殊组件处理
- keepAlive 组件的特殊处理
-
上下文传递
- 继承父组件的 slotScopeIds
- 维护父子组件关系
Teleport 节点
源码实现
const processTeleport = (
n1: TeleportVNode | null,
n2: TeleportVNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
internals: RendererInternals
) => {
// 1. 获取目标容器
const targetSelector = n2.props && n2.props.to;
const target =
typeof targetSelector === "string"
? document.querySelector(targetSelector)
: targetSelector;
// 2. 处理传送
if (n1 == null) {
// 首次挂载
mountChildren(
n2.children as VNodeArrayChildren,
target || container,
null,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
} else {
// 更新处理
patchChildren(
n1,
n2,
target || container,
null,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized
);
}
};
特殊处理说明
-
传送机制
- 目标容器处理
- 子节点传送
-
更新策略
- 目标变更处理
- 子节点更新
- 事件处理
Suspense 节点
源码实现
const processSuspense = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals
) => {
if (n1 == null) {
// 1. 首次挂载
mountSuspense(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
rendererInternals
);
} else {
// 2. 更新处理
patchSuspense(
n1,
n2,
container,
anchor,
parentComponent,
slotScopeIds,
optimized,
rendererInternals
);
}
};
使用示例
<Suspense>
<!-- 异步组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- 加载状态 -->
<template #fallback>
<LoadingComponent />
</template>
</Suspense>
特殊处理说明
-
异步内容处理
- 异步组件加载
- 内容就绪检测
- 状态切换管理
-
更新策略
- 内容变化检测
- 状态保持
- 子树更新优化
-
性能优化
- 缓存已解析内容
- 避免不必要的重渲染
- 优化状态切换
处理策略比较
1. 更新策略
节点类型 | 首次挂载 | 更新处理 | 优化机制 | 特殊处理 |
---|---|---|---|---|
Text | 创建文本节点 | 直接更新内容 | 无 | 无 |
Comment | 创建注释节点 | 不支持更新 | 无 | 开发环境保留 |
Static | 一次性挂载 | 开发环境才更新 | 编译时优化 | 使用锚点标记范围 |
Fragment | 创建锚点 | 可能优化更新 | blocktree优化 | 处理多根节点 |
Element | 创建 DOM 元素 | 属性和子节点 diff | blocktree优化 | 命名空间处理 |
Component | 创建组件实例 | 状态驱动更新 | Props 优化 | 生命周期管理 |
Teleport | 创建传送内容 | 目标更新处理 | 复用 DOM | 跨层级渲染 |
Suspense | 创建异步边界 | 状态切换更新 | 内容缓存 | 嵌套处理 |
2. 特殊处理
-
命名空间处理
- Element 节点需要处理 svg/mathml
- 其他节点类型不需要特殊命名空间
-
作用域处理
- Fragment 需要处理插槽作用域合并
- Component 需要处理插槽作用域
- Element 需要继承作用域
- Teleport 需要处理目标作用域
-
生命周期管理
- Component 完整生命周期钩子
- Suspense 异步生命周期
-
缓存策略
- Static 编译时缓存
- Suspense 异步内容缓存
-
更新优化
- Static 节点依赖编译优化
- Fragment blocktree优化
- Element blocktree优化
- Component Props 更新优化
- Teleport 目标 DOM 复用
-
特殊功能处理
- Teleport 跨层级渲染
- Suspense 异步加载处理
总结
通过本节的分析,我们了解到vue的虚拟节点类型众多,节点的处理方法也各不相同。因此针对不同的节点也有不同的优化手段。下一节,我们来分析component节点的具体处理。
待深入分析的函数
- patchKeyedChildren: 带 key 子节点的 diff 算法
- patchUnkeyedChildren: 无 key 子节点的 diff 算法
- mountComponent: 组件挂载流程
- updateComponent: 组件更新流程