
虚拟 DOM 是什么
VNode(virtual node,虚拟节点)是一个用纯 JS 对象描述真实 DOM 节点的结构。虚拟 DOM 是由这些 VNode 组成的树。
VNode 树相当于一张工程图纸,描述真实 DOM 的结构,但它自己不是真实 DOM。
虚拟 DOM 的作用
直接操纵真实 DOM 的成本很高。一个空的 div 就自带几百个属性和方法,频繁创建、修改、删除都会付出代价。
虚拟 DOM 的思路是把计算搬到 JS 里:先在 JS 中算出差异,最后一步才去碰真实 DOM。
- 减少真实 DOM 的操作次数
- 为 diff 算法提供对比基础
- 不依赖浏览器环境,可以渲染到小程序、iOS、Android、桌面端
渲染流程
- 模板
<template>编译成 render 函数 - render 执行,生成 VNode 树
- patch 函数对比新旧 VNode
- 只把变化的部分更新到真实 DOM
一个简化的实现
下面是教学示意,用来理解流程,不是 Vue 的实现:
// 1. 创建虚拟节点function h(type, props, children) { return { type, props, children }}
// 2. 虚拟 DOM 转真实 DOMfunction createDOM(vnode) { const el = document.createElement(vnode.type) for (const key in vnode.props) el.setAttribute(key, vnode.props[key]) if (vnode.children) { vnode.children.forEach((child) => { el.appendChild(createDOM(child)) }) } return el}对比更新的过程放到下一节。
diff 算法的五个步骤
diff 要解决的问题是:给定新旧两个子节点数组,算出哪些要删、哪些要增、哪些要移动、哪些不用动。
Vue 3 的 patchKeyedChildren 分成五步。下面用 i 表示起始下标,e1 表示旧数组的结束下标,e2 表示新数组的结束下标。
- 从头部同步:
i从 0 开始,新旧节点类型相同时调用 patch 更新并让i自增;类型不同就停下。 - 从尾部同步:从两个数组的末尾往前比对,类型相同就 patch 更新并让
e1、e2各减一;不同就停下。 - 公共序列加挂载:当
e1 < i <= e2时,[i, e2]里全是新节点,逐个挂载到nextPos(下一个待处理位置)之前。 - 公共序列加卸载:当
e2 < i <= e1时,[i, e1]里全是待删除的旧节点,逐个卸载。 - 未知序列:先为新子节点建立
key → index映射。再遍历[i, e1]的旧节点,能找到 key 就 patch,找不到就卸载,同时记录已见过的最大新索引。有移动时用getSequence()求最长递增子序列。最后倒序遍历该区间,挂载新节点,其余节点按需移动。
和第 1、2、3、4 步相比,第 5 步才是通用情况。前面的双端预处理在 Vue 2 里也有,最长递增子序列是 Vue 3 才加的优化。
v-for 与 key
key 是 diff 识别节点身份的凭据。在 v-for 里尽量写上它,但它是推荐项,不是强制项。
不写 key 时,diff 无法识别节点身份,只能按位置原地复用(in-place patch)。这种默认模式本身开销低,但只适合渲染结果不依赖子组件状态或临时 DOM 状态(比如表单输入值)的场景。索引顺序一变,复选框、输入框的状态就会串。
key 要求是 number | string | symbol,同一父节点下必须唯一,不能用对象。
index 作为 key 的问题
用 index 作 key 并不违反硬性要求,硬性要求只有上面那两条。但实践中应该避开,原因是它在重排时不提供任何信息。
用 index 作 key 时,列表的 key 序列在重排前后都是 0..n-1,完全相同。diff 找不到移动依据,只能退化成原地复用——效果等于没写 key,列表状态照样会串。用业务上稳定的 ID 才能让 diff 按身份复用和移动节点,避免无谓的销毁重建。
延伸阅读:这篇 B 站视频 讲 Vue 3 diff,属第三方内容,不是 Vue 官方渠道。
Vue 3 学习笔记
连载中分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 214 天
部分内容可能已过时