理解 Vue 2 中虚拟 DOM(VDOM)的实现原理和 Diff 算法的核心机制,包括 VNode 的创建、patch 流程、以及双端 diff 算法的实现细节。
vue版本:以
vue@2.7.16代码为参考,可能会包含部分 vue3 polyfill 代码。
VDOM 存在的原因
- DOM 性能开销过大:直接操作 DOM 的性能开销比较大,如果修改 DOM 树去执行更新,可能会引起回流和重绘,VDOM 通过维护一个 VDOM 树,通过 diff 和 patch 优化更新流程,计算出最小化更新操作,再一次性同步到真实 DOM 状态,减少 DOM 操作次数。
- 开发人员关注如何编写代码:VDOM 给开发者提供声明式的 UI 编程模型,让开发人员可以把关注点从"如何编写改变 UI 的代码"转变为 “如何描述 UI 的最终状态”,让框架本身去负责 DOM 的更新操作。
- 跨平台:VDOM 是平台无关的 JS 对象结构,小程序、web、uniapp 都可以利用 VDOM 抽象结构来让渲染器消费。
- 细粒度可控和优化空间:key 可以帮助识别列表节点复用,避免重建节点,后续只需更新。vue3 中引入了静态节点优化,同时引入了异步更新队列 nextTick 来支持批量更新操作。
其中 diff 算法是 VDOM 中最核心的部分。
h 函数/createElement 函数
在 Vue 2 中,用户编写 template 模板语法,之后模板语法会被编译成 render 函数,render 函数内部会调用 createElement() 去渲染节点。
h() 函数是在编写函数式组件时用到的渲染函数。
// 创建带子元素的元素
h("div", { class: "container" }, [h("span", "Child 1"), h("span", "Child 2")]);
在 Vue 2 中,h() 是对 createElement() 的再封装:
// src/v3/h.ts
export function h(type: any, props?: any, children?: any) {
if (!currentInstance) {
return createElement(currentInstance!, type, props, children, 2, true)
}
}
// src/shared/util.ts
export function isPrimitive(value: any): boolean {
return (
typeof value === 'string' ||
typeof value === 'number' ||
// $flow-disable-line
typeof value === 'symbol' ||
typeof value === 'boolean'
)
}
// src/core/vdom/create-element.ts
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 判断传入的 data 是否为数组还是原始值
if (isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement(
context: Component,
tag?: string | Component | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 1. 检查 data 是否为响应式对象,是的话不应该作为 vnode 的data对象使用(不应该使用)
if (isDef(data) && isDef((data as any).__ob__)) {
return createEmptyVNode()
}
// 2. 处理 :is 指令
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
// 3. 处理 :is 为 falsy 值的情况
if (!tag) {
return createEmptyVNode()
}
// 4. 处理 scoped slot(单个函数作为 children)
if (isArray(children) && isFunction(children[0])) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
// 5. 规范化 children
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// 6. 创建 VNode
let vnode, ns
// 字符串标签
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 平台内置元素(div, span 等)
// platform built-in elements
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
} else if (
(!data || !data.pre) &&
isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
) {
// 组件
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 未知元素或命名空间元素
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(tag, data, children, undefined, undefined, context)
}
} else {
// 直接传入组件选项或构造函数
// direct component options / constructor
vnode = createComponent(tag as any, data, context, children)
}
// 7. 应用命名空间
if (isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
VNode
VNode(Virtual Node)是用于描述 DOM 节点的 JavaScript 对象。
概念理解
VNode:虚拟节点,是 Vue 用来描述真实 DOM 节点的轻量级 JavaScript 对象,包含了节点的所有必要信息,但不包含实际的 DOM 操作。
VNode 通常包含以下属性:
- 当前 VNode 的标签名或者组件
- 属性和事件
- 子 vnode 数组
- 文本内容
- 对应的真实 DOM 节点
- key(唯一标识)
- context(组件实例)
const vnode = {
tag: "div", // 标签名或组件
data: {
// 属性、事件等
attrs: { id: "app" },
on: { click: handler },
},
children: [], // 子 VNode
text: undefined, // 文本内容
elm: undefined, // 对应的真实 DOM
key: "unique-key", // 唯一标识
context: vm, // 组件实例
// ... 其他属性
};
Vue 2 的 VDOM 和 Diff 算法
当数据发生改变时,响应式属性的 setter 方法会调用 Dep.notify 通知所有订阅者 Watcher,订阅者就会调用 patch 给真实的 DOM 打补丁,更新相应的视图。
进入 patch 步骤的流程
在 Vue 中会有两个步骤会走到 patch:
- 首次渲染:
mountComponent() → Watcher → updateComponent() → patch()。 - 数据更新:
setter → Watcher → updateComponent() → patch()。
mountComponent() 中会执行 new Watcher() 操作。
Watcher 将 updateComponent() 作为回调函数传入,可以看到是调用 。_update(vm._render()) 获取新的 VNode 后执行更新操作。
// src/core/instance/lifecycle.ts
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
vm.$el = el;
if (!vm.$options.render) {
// @ts-expect-error invalid type
vm.$options.render = createEmptyVNode;
}
callHook(vm, "beforeMount");
let updateComponent;
// 核心关键,当组件更新时,会触发该方法!!!
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
const watcherOptions: WatcherOptions = {
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, "beforeUpdate");
}
},
};
// watcher 初始化
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
);
hydrating = false;
// flush buffer for flush: "pre" watchers queued in setup()
const preWatchers = vm._preWatchers;
if (preWatchers) {
for (let i = 0; i < preWatchers.length; i++) {
preWatchers[i].run();
}
}
// 挂载相关
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, "mounted");
}
return vm;
}
在 _update() 的内部,就调用了 __patch__() 比对节点去完成更新。
// src/core/instance/lifecycle.ts
Vue.prototype._update = function (vnode) {
const prevVNode = this._vnode;
this._vnode = vnode;
if (!prevVNode) {
// 初次渲染
this.$el = this.__patch__(this.$el, vnode);
} else {
// 更新
this.$el = this.__patch__(prevVNode, vnode);
}
};
__patch__() 来源于:
//src/platforms/web/runtime/index.ts
// 在 web 平台创建 patch 函数
Vue.prototype.__patch__ = inBrowser ? patch : noop;
// src/platforms/web/runtime/patch.ts
import * as nodeOps from "web/runtime/node-ops";
import { createPatchFunction } from "core/vdom/patch";
import baseModules from "core/vdom/modules/index";
import platformModules from "web/runtime/modules/index";
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules);
// 最终返回了 patch 函数
export const patch: Function = createPatchFunction({ nodeOps, modules });
patch:vnode 新旧节点更新的入口
patch 函数是 VNode 更新的入口,主要处理以下四种情况:
- 无新节点,旧节点销毁:没有新节点,直接触发旧节点的
destroy()钩子,销毁 vnode。 - 无旧节点,新节点创建:没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用
createElm()。 - 新旧节点一致,执行 patchVnode 更新 VNode:旧节点和新节点自身一样,通过
sameVnode()判断节点是否一样,一样时,直接调用patchVnode()去处理这两个节点。 - 新旧节点不一致,销毁旧 VNode,插入新 VNode:旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点。
//src/core/vdom/patch.ts
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
// 没有新节点,直接执行 destroy 钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
return;
}
let isInitialPatch = false;
const insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue); // 没有旧节点,直接用新节点生成 DOM 元素
} else {
const isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点和新节点自身一样,一致执行 patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
// 否则直接销毁旧节点,根据新节点生成 DOM 元素
if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR);
hydrating = true;
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true);
return oldVnode;
}
}
oldVnode = emptyNodeAt(oldVnode);
}
return vnode.elm;
}
}
}
patchVnode:新旧同节点的内容更新过程
patchVnode 主要做了几个判断:
- 新节点是否是文本节点:如果是,则直接更新 DOM 的文本内容为新节点的文本内容。
- 新节点和旧节点如果都有子节点:则处理比较更新子节点,进入双端 diff 流程。
- 只有新节点有子节点,旧节点没有:那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新 DOM,并且添加进父节点。
- 只有旧节点有子节点而新节点没有:说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把 DOM 删除。
function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点一致,什么都不做
if (oldVnode === vnode) {
return;
}
// 假如已创建,克隆复用VNode
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
// 让 vnode.elm 引用到现在的真实 DOM,当 elm 修改时,vnode.elm 会同步变化
const elm = (vnode.elm = oldVnode.elm);
// 异步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return;
}
// 如果新旧都是静态节点,并且具有相同的 key
// 当 vnode 是克隆节点或是 v-once 指令控制的节点时,只需要把 oldVnode.elm 和 oldVnode.child 都复制到 vnode 上
// 也不用再有其他操作
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return;
}
let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
// 如果 vnode 不是文本节点或者注释节点
if (isUndef(vnode.text)) {
// 并且都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用 updateChildren
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
// 如果只有新的 vnode 有子节点
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
// elm 已经引用了老的 DOM 节点,在老的 DOM 节点上添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
// 如果新 vnode 没有子节点,而旧 vnode 有子节点,直接删除老的 oldCh
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, "");
}
// 如果新 vnode 和老 vnode 是文本节点或注释节点
// 但是 vnode.text != oldVnode.text 时,只需要更新 vnode.elm 的文本内容就可以
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}
}
updateChildren:新旧 vnode 子节点的 diff 算法比较流程
通过比较新旧 vnode 子节点,就会通过双端 diff 来比较同层节点。
当节点一致时,就会递归调用 patchVnode() 进行更新。
Vue 2 通过四个指针(新旧节点的起始和结束位置)进行比较,尽可能复用节点:
- 当新老
VNode节点的start相同时,直接patchVnode(),同时新老VNode节点的开始索引都加 1。 - 当新老
VNode节点的end相同时,同样直接patchVnode(),同时新老VNode节点的结束索引都减 1。 - 当老
VNode节点的start和新VNode节点的end相同时,这时候在patchVnode()后,还需要将当前真实 DOM 节点移动到oldEndVnode的后面,同时老VNode节点开始索引加 1,新VNode节点的结束索引减 1。 - 当老
VNode节点的end和新VNode节点的start相同时,这时候在patchVnode()后,还需要将当前真实 DOM 节点移动到oldStartVnode的前面,同时老VNode节点结束索引减 1,新VNode节点的开始索引加 1。 - 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
- 从旧的
VNode为key值,对应index序列为value值的哈希表中找到与newStartVnode一致key的旧的VNode节点,再进行patchVnode(),同时将这个真实 DOM 移动到oldStartVnode对应的真实 DOM 的前面。 - 调用
createElm()创建一个新的 DOM 节点放到当前newStartIdx的位置。
- 从旧的
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode的第一个child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
let newStartVnode = newCh[0] // newVnode的第一个child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一个child不存在
if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最后一个child不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode 和 newStartVnode 是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode 和 newStartVnode,索引左移,继续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode 和 newEndVnode 是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode 和 newEndVnode,索引右移,继续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode 和 newEndVnode 是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode 和 newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果 removeOnly 是 false,则将 oldStartVnode.elm 移动到 oldEndVnode.elm 之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart 索引右移,newEnd 索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果 oldEndVnode 和 newStartVnode 是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode 和 newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果 removeOnly 是 false,则将 oldEndVnode.elm 移动到 oldStartVnode.elm 之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd 索引左移,newStart 索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在 oldChildren 中寻找和 newStartVnode 的具有相同的 key 的 Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,说明 newStartVnode 是一个新的节点
if (isUndef(idxInOld)) { // New element
// 创建一个新 Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和 newStartVnode 具有相同的 key 的 Vnode,叫 vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
// 比较两个具有相同的 key 的新节点是否是同一个节点
// 不设 key,newCh 和 oldCh 只会进行头尾两端的相互比较,设 key 后,除了头尾两端的比较外,还会从用 key 生成的对象 oldKeyToIdx 中查找匹配的节点,所以为节点设置 key 可以更高效的利用 DOM
if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove 和 newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果 removeOnly 是 false,则将找到的和 newStartVnode 具有相同的 key 的 Vnode,叫 vnodeToMove.elm
// 移动到 oldStartVnode.elm 之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果 key 相同,但是节点不相同,则创建一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
四种规则的具体原因和作用
假设:
- 旧列表(oldVnodes):指针
oldStartIdx(开始索引)、oldEndIdx(结束索引)。 - 新列表(newVnodes):指针
newStartIdx(开始索引)、newEndIdx(结束索引)。 - 节点复用的判断:
key相同(且标签名等基础属性一致)。
1. 当 oldStartVnode.key === newStartVnode.key(新旧开始节点相同)
- 操作:直接
patchVnode()(复用节点,更新内容),然后oldStartIdx++、newStartIdx++。 - 原因:这是最理想的情况 —— 新旧列表头部节点完全一致,无需移动 DOM,直接更新内容即可。例如:旧列表
[A, B, C],新列表[A, B, D],首节点A相同,直接复用并后移指针。
2. 当 oldEndVnode.key === newEndVnode.key(新旧结束节点相同)
- 操作:直接
patchVnode(),然后oldEndIdx--、newEndIdx-- - 原因:与规则 1 对称,尾部节点相同,无需移动,直接更新内容。例如:旧列表
[A, B, C],新列表[D, B, C],尾节点C相同,复用并前移指针
3. 当 oldStartVnode.key === newEndVnode.key(旧开始节点 = 新结束节点)
- 操作:
patchVnode()后,将旧开始节点对应的真实 DOM 移动到旧结束节点的后面,然后oldStartIdx++、newEndIdx--。 - 原因:这种情况说明该节点在新列表中被 “移到了尾部”。通过一次移动操作(而非删除再新增)复用节点,减少 DOM 操作成本。例如:旧列表
[A, B, C],新列表[B, C, A],旧首A与新尾A匹配,此时只需把A移动到C后面,即可符合新列表结构。
4. 当 oldEndVnode.key === newStartVnode.key(旧结束节点 = 新开始节点)
- 操作:
patchVnode()后,将旧结束节点对应的真实 DOM 移动到旧开始节点的前面,然后oldEndIdx--、newStartIdx++。 - 原因:与规则 3 对称,该节点在新列表中被 “移到了头部”,通过一次移动操作复用节点。例如:旧列表
[A, B, C],新列表[C, A, B],旧尾C与新首C匹配,把C移动到A前面即可。
双端 diff 算法的优势:
- 优先处理 “无需移动” 的节点:规则 1 和 2 直接复用头部 / 尾部相同的节点,避免无效操作。
- 用最少移动解决 “顺序调整”:规则 3 和 4 针对节点位置互换的场景,通过一次移动即可完成位置修正,比 “删除 + 新增” 高效得多(DOM 移动的成本远低于删除和创建)。
- 减少遍历范围:每处理完一个节点,指针就向中间收缩,逐步缩小对比范围,避免全量遍历(时间复杂度从 O(n²) 优化为 O(n))。
思考
1. Vue2 中所指的同层节点比较,是指哪一块?
同层节点比较,发生在同个新旧 vnode 节点的子节点 children 比较阶段,即双端 diff 阶段,不会发生跨层级节点的比较。
2. 为何要采用双端 diff?
如果是正常的子节点 diff,我们需要遍历新节点,然后再嵌套遍历旧节点,即 Vue 设计与实现中讲到的简单 diff 流程,时间复杂度是 O(n²),使用双端 diff,首尾指针比对,可以把实现复杂度压缩到 O(n)。
总结
- VDOM 存在的价值:通过维护虚拟 DOM 树,减少直接操作真实 DOM 的性能开销,提供声明式编程模型,支持跨平台渲染。
- Diff 算法流程:数据变化触发
setter → Watcher → updateComponent() → patch() → patchVnode() → updateChildren(),通过双端 diff 算法高效比较和更新节点。 - 双端 diff 算法:使用四个指针(新旧节点的起始和结束位置)进行比较,通过四种匹配规则优先处理无需移动的节点,用最少移动解决顺序调整,时间复杂度从 O(n²) 优化为 O(n)。
- key 复用节点:key 属性帮助识别相同节点并复用,避免不必要的重建。
参考内容
-
Vue 设计与实现 - 第 10 章 双端 diff 算法
1355

被折叠的 条评论
为什么被折叠?



