Vue中的虚拟DOM、diff算法、key的作用详解
作者:猫老板的豆
一、虚拟DOM
从本质上来说,虚拟DOM是一个JavaScript对象,通过对象的形式来描述真实的DOM结构。
将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
虚拟DOM本身是js对象,是对DOM的抽象,是更加轻量级的对 DOM的描述。
虚拟DOM流程
创建虚拟 DOM:
当组件被创建或更新时,Vue 的渲染函数会返回一个新的虚拟 DOM 树。
这个树是一个 JavaScript 对象,它描述了 真实DOM 的结构、属性和子元素。
// 真实dom
<ul id="list">
<li class="item">哈哈</li>
<li class="item">呵呵</li>
<li class="item">嘿嘿</li>
</ul>Vue会把代码转换成一个对象(虚拟 DOM),通过对象的形式来描述真实的DOM结构
转换成虚拟 DOM对象, 类似于下面这种(伪代码)
let oldVDOM = { // 旧虚拟DOM
tagName: 'ul', // 标签名
props: { // 标签属性
id: 'list'
},
children: [ // 标签子节点
{
tagName: 'li', props: { class: 'item' }, text: '哈哈'
},
{
tagName: 'li', props: { class: 'item' }, text: '呵呵'
},
{
tagName: 'li', props: { class: 'item' }, text: '嘿嘿'
},
]
}比较虚拟 DOM:
这时候,我修改一个li标签的文本:
<ul id="list">
<li class="item">哈哈</li>
<li class="item">呵呵</li>
<li class="item">哈哈哈</li> // 修改
</ul>这时候生成的新虚拟DOM为:
let newVDOM = { // 新虚拟DOM
tagName: 'ul', // 标签名
props: { // 标签属性
id: 'list'
},
children: [ // 标签子节点
{
tagName: 'li', props: { class: 'item' }, text: '哈哈'
},
{
tagName: 'li', props: { class: 'item' }, text: '呵呵'
},
{
tagName: 'li', props: { class: 'item' }, text: '哈哈哈'
},
]
}- Vue会对比新旧虚拟DOM之间的差异,对比出是哪个虚拟节点更改了,找出这些差异。
- 这个比较过程会找出两个树之间的最小变化集,即哪些节点需要被添加、更新或删除。
- 这个过程被称为“diffing”或“差异算法”。Vue 的差异算法是高度优化的,可以快速地计算出两个树之间的差异。
应用更改:
一旦差异被计算出来:
- Vue 就会只更新这些有差异的节点应用到实际的 DOM 上,而不用更新其他数据没发生改变的节点,实现精准地更新真实DOM,进而提高效率。
- 这个过程是批量的,意味着多个更改可以一次性地应用到 DOM 上,从而减少了浏览器的重排和重绘次数。
- 这个过程被称为 Patch 或“打补丁”
更新视图:
当 DOM 更新完成后,浏览器就会重新渲染视图,以反映组件的最新状态。
虚拟DOM优点
- 性能优化:通过减少对实际 DOM 的操作次数,虚拟 DOM 可以显著提高性能。
- 跨平台:虚拟 DOM 使得 Vue.js 可以轻松地实现跨平台应用,例如使用 Web Workers 或 Weex 等技术来创建桌面应用或移动应用。
- 易于测试和调试:由于虚拟 DOM 是 JavaScript 对象,因此它可以使用 JavaScript 的所有调试工具进行测试和调试。此外,你还可以使用诸如 Jest 或 Mocha 等测试框架来编写针对虚拟 DOM 的单元测试。
用虚拟DOM算法操作真实DOM,性能高于直接操作真实DOM
虚拟DOM和虚拟DOM算法是两种概念:虚拟DOM算法 = 虚拟DOM + Diff算法
二、diff 算法 ⚡️
上面的例子中,其实只有一个li标签修改了文本,其他都是不变的,所以没必要所有的节点都要更新,只更新这个li标签就行,Diff算法就是查出这个li标签的算法。
Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点,实现精准地更新真实DOM,进而提高效率。
- 使用虚拟DOM算法的损耗计算: 总损耗 = 虚拟DOM增删改+(与Diff算法效率有关)真实DOM差异增删改+(较少的节点)排版与重绘
- 直接操作真实DOM的损耗计算: 总损耗 = 真实DOM完全增删改+(可能较多的节点)排版与重绘
⚡️ diff 算法是一种优化手段:将新旧两个虚拟 DOM 树进行差异对比,修补(更新)差异的过程叫做patch(打补丁),将更新补丁作用于真实 DOM,以最小成本完成视图更新。
- Diff 算法:当组件的数据发生变化时,Vue.js 会创建一个新的虚拟 DOM 树来反映这些变化。然后,它会使用 diff 算法来比较旧的虚拟 DOM 树和新的虚拟 DOM 树之间的差异。这个比较过程会找出两个树之间的最小变化集,即哪些节点需要被添加、更新或删除。
- Patch 过程(打补丁):在确定了需要更新的节点之后,Vue.js 会进入 patch 过程。这个过程会将差异(或称为“补丁”)应用到实际的 DOM 上。通过只更新发生变化的节点,Vue.js 可以避免不必要的 DOM 操作,从而提高性能。
- 最小成本视图更新:通过只更新发生变化的节点,Vue.js 能够在不重新渲染整个页面的情况下更新视图。这大大降低了浏览器的重排和重绘次数,从而提高了页面的性能。
为什么 vue ,react 这些框架中都会有 diff 算法呢?
要知道渲染真实 DOM 的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实 DOM 上会引起整个 DOM 树的重绘和重排,有没有可能我们只更新我们修改的那一小块 DOM 而不要更新整个 DOM 呢?diff 算法能够帮助我们。
当数据发生变化时,vue是怎么更新节点的?
我们先根据真实 DOM 生成一棵 virtual DOM(虚拟DOM)树,当 virtual DOM 某个节点的数据改变后会生成一个新的 Vnode(虚拟节点),然后 Vnode 和 oldVnode 作对比,发现有不一样的地方就直接修改在真实的 DOM 上,然后使 oldVnode 的值为Vnode。
diff 的过程就是调用名为 patch 的函数,比较新旧节点,一边比较一边给真实的 DOM 打补丁。
diff算法流程
在Vue中,DIFF算法(也称为差异算法或对比算法)主要用于比较新旧虚拟DOM(Virtual DOM)之间的差异,并基于这些差异来最小化地更新真实的DOM,从而提高页面渲染的性能。
1. 虚拟虚拟DOM
Vue在渲染过程中,首先会创建一个虚拟的DOM树(VNode),这个树是对真实DOM的一个抽象表示。
当数据发生变化时,Vue会生成一个新的虚拟DOM树,并与旧的虚拟DOM树进行比较。
2. 对比虚拟DOM
diff算法会比较新旧两个虚拟DOM树,找出它们之间的差异。这个比较过程是在同层级进行的,不会跨层级比较。
比较过程中,Vue会采用一系列优化策略,如双指针、头尾比较等,以提高效率。
- Vue会比较新旧虚拟DOM节点的类型和属性,以及它们的子节点。
- 如果两个节点相同(即类型和属性都相同),则不需要进行任何操作。
- 如果节点类型不同,或者属性发生了变化,Vue会创建一个新的DOM节点来替换旧的节点。
- 对于子节点的比较,Vue会递归地应用DIFF算法。
3. patch打补丁
一旦diff算法找出新旧虚拟DOM之间的差异,Vue会生成一个patch对象,这个对象记录了需要执行的DOM操作。
然后,Vue会遍历这个patch对象,执行相应的DOM操作,更新真实的DOM。
diff算法优化策略
1. 比较只会在同层级进行, 不会跨层级比较
只会在同层级的节点之间进行比较,不会跨层级比较。这大大减少了比较的次数和复杂度。
如果DOM节点出现了跨层级操作,Diff算法会简单地视为删除旧节点并创建新节点,而不是尝试去移动节点。
<div>
<p>123</p>
</div><div>
<span>456</span>
</div>上面的代码会分别比较同一层的两个div以及第二层的p和span,但是不会拿div和span作比较。
一张很形象的图:(比较只会在同层级进行, 不会跨层级比较)

概括起来就是对操作前后的dom树同一层的节点进行对比,一层一层对比,然后再插入真实的dom中,重新渲染

2. 深度优先策略
diff算法在比较过程中采用深度优先策略,深度优先策略意味着算法会先尽可能深地搜索树的分支,直到到达某个节点所在的最深位置,然后再回溯到上一层,继续搜索其他分支。
当比较两个VNode时,如果它们都有子节点,那么diff算法会首先比较这两个VNode的子节点。这个过程会递归地应用深度优先策略,先比较子节点的子节点,直到到达树的叶子节点。
3. 双指针策略
双指针策略是指在比较新旧VNode时,同时设置两个指针,一个指向旧VNode的头部,另一个指向新VNode的头部。这两个指针会同时移动,以找出两个VNode树中的相同节点或差异。
- 移动指针:在比较过程中,如果当前节点相同(即类型和属性都相同),则两个指针都会向后移动,继续比较它们的子节点。
- 节点复用:如果找到了相同的节点,Vue会复用该节点,而不是重新创建。这样可以避免不必要的DOM操作,提高性能。
- 处理差异:当两个指针指向的节点不同时,Vue会根据差异类型进行相应的处理,如添加新节点、删除旧节点或更新节点。
4. 头尾比较
头尾比较策略是在双指针策略的基础上的一种优化。它同时从新旧VNode的头部和尾部开始比较,以快速找到相同且不需要移动的节点。
从两头向中间比较:从新旧VNode的头部和尾部同时开始比较,如果找到相同的节点,则将这些节点标记为已处理,并继续向中间移动指针。

快速定位相同节点:由于头尾比较策略同时从两端开始,因此可以更快地定位到相同且不需要移动的节点。这样可以减少不必要的比较和DOM操作,提高性能。
处理剩余节点:当头部和尾部的指针相遇或交叉时,表示已经处理完了所有相同的节点。此时,如果新VNode中还有剩余节点,Vue会将这些节点添加到真实DOM的末尾;如果旧VNode中还有剩余节点,Vue会将这些节点从真实DOM中删除。
5. 跳过静态节点
Vue会对模板中的静态内容进行优化,以减少更新时的性能消耗。
例如,如果一个节点的内容是静态的(即不会随数据变化而变化),Vue会将其标记为静态节点,并在后续的DIFF过程中跳过这些节点的比较。
通过以上优化策略,diff算法可以更加高效地找出新旧VNode之间的差异,并以最小化的代价更新真实的DOM。这些优化策略使得Vue在处理大量DOM更新时能够保持较高的性能。
6. 唯一标识key
详见下文
三、vue for 循环中 key 的作用
key是给每一个vnode的唯一id,可以根据key,更准确、 更快的找到对应的vnode节点。帮助Vue跟踪每个节点的身份,以便在数据改变时能够高效地更新虚拟DOM。
在新旧虚拟DOM的对比过程中看是否能找到相同的key:
1.有相同的key:(Vue就会认为它们是同一个节点)
- 若虚拟DOM中的内容没变,则直接复用之前的真实DOM
- 若虚拟DOM中的内容变了,则生产新的真实DOM,并替换掉旧的真是DOM
2.没有相同的key:(Vue就会认为是新节点)
- 根据数据创建新的真是DOM,随后渲染到页面
vue中列表循环需加:key="唯一标识" 唯一标识可以是 item 里面 id 等
因为vue组件高度复用增加 Key 可以标识组件的唯一性,那么 Key 是如何更高效的更新虚拟 DOM 的呢,我们看下面的例子:
<body>
<div id="demo">
<p v-for="item in items" :key="item">{{item}}</p>
</div>
<script src="../../dist/vue.js"></script>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: { items: ['a', 'b', 'c', 'd', 'e'] },
mounted () {
setTimeout(() => {
this.items.splice(2, 0, 'f') // ['a', 'b', 'f', 'c', 'd', 'e']
}, 2000);
},
});
</script>
</body>在不使用key的情况,vue会进行这样的操作:

分析下整体流程:
- 比较A,A,相同类型的节点,进行patch,但数据相同,不发生dom操作
- 比较B,B,相同类型的节点,进行patch,但数据相同,不发生dom操作
- 比较C,F,相同类型的节点,进行patch,数据不同,发生dom操作
- 比较D,C,相同类型的节点,进行patch,数据不同,发生dom操作
- 比较E,D,相同类型的节点,进行patch,数据不同,发生dom操作
- 循环结束,将E插入到DOM中 一共发生了3次更新,1次插入操作
我们希望可以在B和C之间加一个F,diff 算法默认执行起来是这样的:即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率?
所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。

当为v-for渲染的列表项设置唯一的key时,Vue能够准确地识别每个列表项的身份。
当列表数据发生变化时,Vue会根据key来判断哪些列表项是新添加的、哪些是被删除的、哪些是需要更新的。 这有助于Vue进行高效的DOM更新操作,只更新需要改变的部分,而不是重新渲染整个列表。
当我们在使用v-for时,需要给单元加上key:
- 如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。
- 如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed
- 用+new Date()生成的时间戳作为key,手动强制触发重新渲染
- 当拥有新值的rerender作为key时,拥有了新key的Comp出现了,那么旧key Comp会被移除,新key Comp触发渲染
四、Vue 2 与 Vue3 中的区别
在 Vue 2 中:
- 使用了虚拟 DOM 的技术来优化 DOM 操作。当数据变化时,Vue 会生成一个新的虚拟 DOM 树,并与旧的虚拟 DOM 树进行比较(这个过程通常称为“diff”)。如果发现有差异,Vue 就会更新真实的 DOM。
- 在 Vue 2 的实现中,这种比较是递归进行的,会遍历整个虚拟 DOM 树,即使只有很少的部分发生了变化。这种策略在某些情况下可能会导致较高的计算复杂度。
- ❗ 补充:虽然理论上要遍历全树,但实际通过同层比较和 key 优化可以跳过部分子树,并非完全无优化。
在 Vue3 中:
区块树(Block Tree) 是Vue 3中引入的一种重要的优化机制,它是Vue 3编译器在解析模板时生成的一种树状结构,它基于模板中的代码逻辑将模板划分为多个区块(Block)。
区块树的主要功能是在渲染时优化DOM操作,它可以将模板中的静态内容和动态内容区分开,从而在更新时只关注那些实际发生变化的动态内容。 通过减少不必要的渲染开销来提高性能。
- 模板解析:Vue 3编译器首先会将模板解析成抽象语法树(AST),然后遍历AST,找出其中的连续节点块(Block),并对这些节点块进行分析和整理,最终形成一个区块树。
- 静态提升(Hoist Static) :在构建区块树的过程中,Vue 3会将静态节点(即不会改变的节点)提升到渲染函数之外,从而减少了在每次渲染时都需要重新创建这些节点的开销,彻底避免重复 diff。
- 标记动态节点类型(PatchFlags ):对于动态节点,Vue 3会在区块树中标记它们的变化类型(例如,属性更新、子节点更新等)。
- 这些标记可以在创建虚拟 DOM 节点时生成,并在后续的 diff 过程中使用。由于有了这些标记,Vue 3 在进行 diff 时就可以更有针对性地比较那些实际发生变化的节点,根据这些标记快速定位到需要更新的节点,并只对这些节点进行更新,而无需遍历整个树。这显著减少了比较的范围和计算量,从而提高了更新性能。
综上:
- 将纯静态节点提升到渲染函数外部,避免重复创建和比较;
- 对动态节点标记 PatchFlags 标识其变化类型(如文本/属性/子节点);
- 配合区块树结构记录动态子节点位置。
- 在 diff 过程中,Vue 3 能够跳过静态节点,并根据 PatchFlags 仅对动态节点的特定属性进行靶向比对,从而大幅降低计算复杂度。
举例:
<!-- 模板 -->
<div>
<span>Static</span> <!-- 静态提升(Hoist Static) -->
<span :class="cls"></span> <!-- 标记动态节点类型(PatchFlags ) class -->
<span>{{ text }}</span> <!-- 标记动态节点类型(PatchFlags ) text-->
</div>- Vue 2:比对 3 个 的所有属性+子节点
- Vue 3:完全跳过第一个静态 span,仅比对第二个 span 的 class 属性和第三个 span 的文本内容
这种机制使得 Vue 3 在具有大量静态内容+局部动态更新的场景下,性能优势尤为明显。
不建议用index作为key
不建议 用 index 作为 key,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是 0, 1, 2 这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作,效率底。
- 用key会出问题:对数据进行破坏顺序的的操作(如:在数组前面或中间添加、删除数据)
- 用key不会出问题:没有对数据进行破坏顺序的的操作(如:在数组最后面添加、删除数据)
建议key用唯一标识,如:id、手机号、学号等
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
