mount函数创建虚拟节点之后就会调用render函数,而render函数内部的核心就是patch函数,也就是大名鼎鼎的diff算法。
const patch: PatchFn = (
n1, // 旧的虚拟节点
n2, // 新的虚拟节点
container, // 容器元素
anchor = null, // 锚点元素
parentComponent = null, // 父组件实例
parentSuspense = null, // 父 Suspense 组件
namespace = undefined, // 命名空间
slotScopeIds = null, // 插槽作用域 ID
optimized = !!n2.dynamicChildren, // 是否优化
) => {
if (n1 === n2) {
return
}
// 1. 处理新旧节点类型不同的情况
if (n1 && !isSameVNodeType(n1, n2)) {
// 1. 获取下一个节点作为锚点
anchor = getNextHostNode(n1)
// 2. 卸载旧节点
unmount(n1, parentComponent, parentSuspense, true)
// 3. 重置旧节点引用
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
// 2. 根据新节点的类型选择不同的处理方式
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:
// 3. 处理组件或元素节点
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) {
// 处理TELEPORT
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals,
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理SUSPENSE
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals,
)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
Vue 中的节点类型可以分为以下几类:
-
基础节点类型
- Text: 文本节点
- Comment: 注释节点
- Static: 静态节点
- Fragment: 片段节点
-
复杂节点类型
- Element: 普通元素节点
- Component: 组件节点
- Teleport: 传送门组件
- Suspense: 异步组件
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,
) => {
// static nodes are only present when used with compiler-dom/runtime-dom
// 静态节点仅在使用 compiler-dom/runtime-dom 时存在
;[n2.el, n2.anchor] = hostInsertStaticContent!(
n2.children as string,
container,
anchor,
namespace,
n2.el,
n2.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>
Fragment 节点处理
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
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
// check if this is a slot fragment with :slotted scope ids
if (fragmentSlotScopeIds) {
slotScopeIds = slotScopeIds
? slotScopeIds.concat(fragmentSlotScopeIds)
: fragmentSlotScopeIds
}
if (n1 == null) {
// 首次挂载:插入锚点并挂载子节点
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
// a fragment can only have array children
// since they are either generated by the compiler, or implicitly created
// from arrays.
mountChildren(
// #10007
// such fragment like `<></>` will be compiled into
// a fragment which doesn't have a children.
// In this case fallback to an empty array
(n2.children || []) as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
// 更新:根据情况选择更新策略
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
// 更新动态块
patchBlockChildren(
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
)
if (
// #2080 if the stable fragment has a key, it's a <template v-for> that may
// get moved around. Make sure all root level vnodes inherit el.
// #2134 or if it's a component root, it may also get moved around
// as the component is being moved.
n2.key != null ||
(parentComponent && n2 === parentComponent.subTree)
) {
traverseStaticChildren(n1, n2, true /* shallow */)
}
} else {
// 全量更新
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
}
使用示例
<!-- 多根节点模板 -->
<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>
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
);
}
};
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);
}
}
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 | 创建异步边界 | 状态切换更新 | 内容缓存 | 嵌套处理 |