在前面的文章中,我们深入探讨了虚拟 DOM 的创建和组件的挂载过程。当数据变化时,Vue 需要高效地更新 DOM。这个过程的核心就是 patch 算法——新旧虚拟 DOM 的比对与更新策略。本文将带你深入理解 Vue3 的 patch 算法,看看它如何以最小的代价完成 DOM 更新。
前言:为什么需要patch?
想象一下,你有一个展示用户列表的页面。当某个用户的名字改变时,我们会怎么做?
- 粗暴方式:重新渲染整个列表(性能差)
- 聪明方式:只更新那个改变的用户名(性能好)
patch 算法就是 Vue 采用的"聪明方式"。它的核心思想是:找出新旧 VNode 的差异,只更新变化的部分,而不是重新渲染整个 DOM 树:
patch函数的核心逻辑
patch的整体架构
patch 函数是整个更新过程的总调度器,它根据节点类型分发到不同的处理函数:
function patch(oldVNode, newVNode, container, anchor = null) {
// 如果是同一个引用,无需更新
if (oldVNode === newVNode) return;
// 如果类型不同,直接替换
if (oldVNode && !isSameVNodeType(oldVNode, newVNode)) {
unmount(oldVNode);
oldVNode = null;
}
const { type, shapeFlag } = newVNode;
// 根据类型分发处理
switch (type) {
case Text:
processText(oldVNode, newVNode, container, anchor);
break;
case Comment:
processComment(oldVNode, newVNode, container, anchor);
break;
case Fragment:
processFragment(oldVNode, newVNode, container, anchor);
break;
case Static:
processStatic(oldVNode, newVNode, container, anchor);
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(oldVNode, newVNode, container, anchor);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(oldVNode, newVNode, container, anchor);
} else if (shapeFlag & ShapeFlags.TELEPORT) {
processTeleport(oldVNode, newVNode, container, anchor);
} else if (shapeFlag & ShapeFlags.SUSPENSE) {
processSuspense(oldVNode, newVNode, container, anchor);
}
}
}
patch 的分发流程图
判断节点类型的关键:isSameVNodeType
function isSameVNodeType(n1, n2) {
// 比较类型和key
return n1.type === n2.type && n1.key === n2.key;
}
为什么需要key?
我们看看下面的例子:
<!-- 旧列表 -->
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
<!-- 新列表 -->
<li key="a">A</li>
<li key="c">C</li>
<li key="b">B</li>
<!-- 有key: 只移动节点,不重新创建 -->
<!-- 无key: 全部重新创建,性能差 -->
不同类型节点的处理策略
文本节点的处理
文本节点是最简单的节点类型,处理逻辑也最直接:
function processText(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
// 首次挂载
const textNode = document.createTextNode(newVNode.children);
newVNode.el = textNode;
container.insertBefore(textNode, anchor);
} else {
// 更新
const el = (newVNode.el = oldVNode.el);
if (newVNode.children !== oldVNode.children) {
// 只有文本变化时才更新
el.nodeValue = newVNode.children;
}
}
}
文本节点更新过程
注释节点的处理
注释节点基本不需要更新,因为用户通常不关心注释的变化:
function processComment(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
const commentNode = document.createComment(newVNode.children);
newVNode.el = commentNode;
container.insertBefore(commentNode, anchor);
} else {
// 注释节点很少变化,直接复用
newVNode.el = oldVNode.el;
}
}
元素节点的处理
元素节点的更新是最复杂的,需要处理属性和子节点:
function processElement(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
// 首次挂载
mountElement(newVNode, container, anchor);
} else {
// 更新
patchElement(oldVNode, newVNode);
}
}
function patchElement(oldVNode, newVNode) {
const el = (newVNode.el = oldVNode.el);
// 1. 更新props
patchProps(el, oldVNode.props, newVNode.props);
// 2. 更新children
patchChildren(oldVNode, newVNode, el);
}
function patchProps(el, oldProps, newProps) {
oldProps = oldProps || {};
newProps = newProps || {};
// 移除旧props中不存在于新props的属性
for (const key in oldProps) {
if (!(key in newProps)) {
patchProp(el, key, oldProps[key], null);
}
}
// 添加或更新新props
for (const key in newProps) {
const old = oldProps[key];
const next = newProps[key];
if (old !== next) {
patchProp(el, key, old, next);
}
}
}
子节点的比对策略
子节点的比对是 patch 算法中最复杂、也最关键的部分。Vue3 根据子节点的类型,采用不同的策略。
子节点类型组合的处理策略
下表总结了所有可能的子节点类型组合及对应的处理方式:
| 旧子节点 | 新子节点 | 处理策略 | 示例 |
|---|---|---|---|
| 文本 | 文本 | 直接替换文本内容 | "old" → "new" |
| 文本 | 数组 | 清空文本,挂载数组 | "text" → [vnode1, vnode2] |
| 文本 | 空 | 清空文本 | "text" → null |
| 数组 | 文本 | 卸载数组,设置文本 | [vnode1, vnode2] → "text" |
| 数组 | 数组 | 执行核心diff | [a,b,c] → [a,d,e] |
| 数组 | 空 | 卸载所有子节点 | [a,b,c] → null |
| 空 | 文本 | 设置文本 | null → "text" |
| 空 | 数组 | 挂载数组 | null → [a,b,c] |
当新旧节点都为数组时,需要执行
diff算法,diff算法的内容在后面的文章中会专门介绍。
Fragment和Text节点的特殊处理
Fragment的处理
Fragment 是 Vue3 新增的节点类型,用于支持多根节点:
function processFragment(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
// 首次挂载
mountFragment(newVNode, container, anchor);
} else {
// 更新
patchFragment(oldVNode, newVNode, container, anchor);
}
}
function mountFragment(vnode, container, anchor) {
const { children, shapeFlag } = vnode;
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 文本子节点:挂载为文本节点
const textNode = document.createTextNode(children);
vnode.el = textNode;
container.insertBefore(textNode, anchor);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 数组子节点:挂载所有子节点
mountChildren(children, container, anchor);
// 设置el和anchor
vnode.el = children[0]?.el;
vnode.anchor = children[children.length - 1]?.el;
}
}
function patchFragment(oldVNode, newVNode, container, anchor) {
const oldChildren = oldVNode.children;
const newChildren = newVNode.children;
// Fragment本身没有DOM,直接patch子节点
patchChildren(oldVNode, newVNode, container);
// 更新el和anchor
if (Array.isArray(newChildren)) {
newVNode.el = newChildren[0]?.el || oldVNode.el;
newVNode.anchor = newChildren[newChildren.length - 1]?.el || oldVNode.anchor;
}
}
文本节点的优化
Vue3 对纯文本节点做了特殊优化,避免不必要的 VNode 创建:
// 模板:<div>{{ message }}</div>
// 编译后:
function render(ctx) {
return h('div', null, ctx.message, PatchFlags.TEXT);
}
// 在patch过程中:
if (newVNode.patchFlag & PatchFlags.TEXT) {
// 只需要更新文本内容,不需要比较其他属性
const el = oldVNode.el;
if (newVNode.children !== oldVNode.children) {
el.textContent = newVNode.children;
}
newVNode.el = el;
return;
}
手写实现:完整的patch函数基础版本
基础工具函数
// 类型标志
const ShapeFlags = {
ELEMENT: 1,
FUNCTIONAL_COMPONENT: 1 << 1,
STATEFUL_COMPONENT: 1 << 2,
TEXT_CHILDREN: 1 << 3,
ARRAY_CHILDREN: 1 << 4,
SLOTS_CHILDREN: 1 << 5,
TELEPORT: 1 << 6,
SUSPENSE: 1 << 7,
COMPONENT_SHOULD_KEEP_ALIVE: 1 << 8,
COMPONENT_KEPT_ALIVE: 1 << 9
};
// 特殊节点类型
const Text = Symbol('Text');
const Comment = Symbol('Comment');
const Fragment = Symbol('Fragment');
// 判断是否同类型节点
function isSameVNodeType(n1, n2) {
return n1.type === n2.type && n1.key === n2.key;
}
完整的patch函数
class Renderer {
constructor(options) {
this.options = options;
}
patch(oldVNode, newVNode, container, anchor = null) {
if (oldVNode === newVNode) return;
// 处理不同类型的节点
if (oldVNode && !isSameVNodeType(oldVNode, newVNode)) {
this.unmount(oldVNode);
oldVNode = null;
}
const { type, shapeFlag } = newVNode;
// 根据类型分发
switch (type) {
case Text:
this.processText(oldVNode, newVNode, container, anchor);
break;
case Comment:
this.processComment(oldVNode, newVNode, container, anchor);
break;
case Fragment:
this.processFragment(oldVNode, newVNode, container, anchor);
break;
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
this.processElement(oldVNode, newVNode, container, anchor);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
this.processComponent(oldVNode, newVNode, container, anchor);
} else if (shapeFlag & ShapeFlags.TELEPORT) {
this.processTeleport(oldVNode, newVNode, container, anchor);
}
}
}
processElement(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
// 挂载
this.mountElement(newVNode, container, anchor);
} else {
// 更新
this.patchElement(oldVNode, newVNode);
}
}
mountElement(vnode, container, anchor) {
const { type, props, children, shapeFlag } = vnode;
// 创建元素
const el = this.options.createElement(type);
vnode.el = el;
// 设置属性
if (props) {
for (const key in props) {
this.options.patchProp(el, key, null, props[key]);
}
}
// 处理子节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
this.options.setElementText(el, children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
this.mountChildren(children, el);
}
// 插入
this.options.insert(el, container, anchor);
}
patchElement(oldVNode, newVNode) {
const el = (newVNode.el = oldVNode.el);
const oldProps = oldVNode.props || {};
const newProps = newVNode.props || {};
// 更新属性
this.patchProps(el, oldProps, newProps);
// 更新子节点
this.patchChildren(oldVNode, newVNode, el);
}
patchChildren(oldVNode, newVNode, container) {
const oldChildren = oldVNode.children;
const newChildren = newVNode.children;
const oldShapeFlag = oldVNode.shapeFlag;
const newShapeFlag = newVNode.shapeFlag;
// 新子节点是文本
if (newShapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
this.unmountChildren(oldChildren);
}
if (oldChildren !== newChildren) {
this.options.setElementText(container, newChildren);
}
}
// 新子节点是数组
else if (newShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (oldShapeFlag & ShapeFlags.TEXT_CHILDREN) {
this.options.setElementText(container, '');
this.mountChildren(newChildren, container);
} else if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
this.patchKeyedChildren(oldChildren, newChildren, container);
}
}
// 新子节点为空
else {
if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
this.unmountChildren(oldChildren);
} else if (oldShapeFlag & ShapeFlags.TEXT_CHILDREN) {
this.options.setElementText(container, '');
}
}
}
processText(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
const textNode = this.options.createText(newVNode.children);
newVNode.el = textNode;
this.options.insert(textNode, container, anchor);
} else {
const el = (newVNode.el = oldVNode.el);
if (newVNode.children !== oldVNode.children) {
this.options.setText(el, newVNode.children);
}
}
}
processFragment(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
this.mountFragment(newVNode, container, anchor);
} else {
this.patchFragment(oldVNode, newVNode, container, anchor);
}
}
mountFragment(vnode, container, anchor) {
const { children, shapeFlag } = vnode;
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
const textNode = this.options.createText(children);
vnode.el = textNode;
this.options.insert(textNode, container, anchor);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
this.mountChildren(children, container, anchor);
vnode.el = children[0]?.el;
vnode.anchor = children[children.length - 1]?.el;
}
}
mountChildren(children, container, anchor) {
for (let i = 0; i < children.length; i++) {
this.patch(null, children[i], container, anchor);
}
}
unmount(vnode) {
const { shapeFlag, el } = vnode;
if (shapeFlag & ShapeFlags.COMPONENT) {
this.unmountComponent(vnode);
} else if (shapeFlag & ShapeFlags.FRAGMENT) {
this.unmountFragment(vnode);
} else if (el) {
this.options.remove(el);
}
}
}
Vue2 与 Vue3 的 patch 差异
核心差异对比表
| 特性 | Vue2 | Vue3 | 优势 |
|---|---|---|---|
| 数据劫持 | Object.defineProperty | Proxy | Vue3可以监听新增/删除属性 |
| 编译优化 | 全量比较 | 静态提升 + PatchFlags | Vue3跳过静态节点比较 |
| diff算法 | 双端比较 | 最长递增子序列 | Vue3移动操作更少 |
| Fragment | 不支持 | 支持 | 多根节点组件 |
| Teleport | 不支持 | 支持 | 灵活的DOM位置控制 |
| Suspense | 不支持 | 支持 | 异步依赖管理 |
| 性能 | 中等 | 优秀 | Vue3更新速度提升1.3-2倍 |
PatchFlags 带来的优化
Vue3 通过 PatchFlags 标记动态内容,减少比较范围:
const PatchFlags = {
TEXT: 1, // 动态文本
CLASS: 2, // 动态class
STYLE: 4, // 动态style
PROPS: 8, // 动态属性
FULL_PROPS: 16, // 全量props
HYDRATE_EVENTS: 32, // 事件
STABLE_FRAGMENT: 64, // 稳定Fragment
KEYED_FRAGMENT: 128, // 带key的Fragment
UNKEYED_FRAGMENT: 256, // 无key的Fragment
NEED_PATCH: 512, // 需要非props比较
DYNAMIC_SLOTS: 1024, // 动态插槽
HOISTED: -1, // 静态节点
BAIL: -2 // 退出优化
};
结语
理解 patch 算法,就像是掌握了 Vue 更新 DOM 的"手术刀"。知道它如何精准地找到需要更新的部分,以最小的代价完成更新,这不仅能帮助我们写出更高效的代码,还能在遇到性能问题时快速定位和优化。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!