vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue的虚拟DOM、diff算法、key作用

Vue中的虚拟DOM、diff算法、key的作用详解

作者:猫老板的豆

这篇文章主要介绍了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: '哈哈哈'
        },
    ]
}

应用更改:

一旦差异被计算出来:

更新视图:

当 DOM 更新完成后,浏览器就会重新渲染视图,以反映组件的最新状态。

虚拟DOM优点

用虚拟DOM算法操作真实DOM,性能高于直接操作真实DOM

虚拟DOM虚拟DOM算法是两种概念:虚拟DOM算法 = 虚拟DOM + Diff算法

二、diff 算法 ⚡️

上面的例子中,其实只有一个li标签修改了文本,其他都是不变的,所以没必要所有的节点都要更新,只更新这个li标签就行,Diff算法就是查出这个li标签的算法

Diff算法是一种对比算法。对比两者是旧虚拟DOM新虚拟DOM,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点,实现精准地更新真实DOM,进而提高效率

⚡️ diff 算法是一种优化手段:将新旧两个虚拟 DOM 树进行差异对比,修补(更新)差异的过程叫做patch(打补丁),将更新补丁作用于真实 DOM,以最小成本完成视图更新。

为什么 vue ,react 这些框架中都会有 diff 算法呢?

要知道渲染真实 DOM 的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实 DOM 上会引起整个 DOM 树的重绘和重排,有没有可能我们只更新我们修改的那一小块 DOM 而不要更新整个 DOM 呢?diff 算法能够帮助我们。

当数据发生变化时,vue是怎么更新节点的?

我们先根据真实 DOM 生成一棵 virtual DOM(虚拟DOM)树,当 virtual DOM 某个节点的数据改变后会生成一个新的 Vnode(虚拟节点),然后 VnodeoldVnode 作对比,发现有不一样的地方就直接修改在真实的 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会采用一系列优化策略,如双指针、头尾比较等,以提高效率。

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树中的相同节点或差异。

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就会认为它们是同一个节点)

2.没有相同的key:(Vue就会认为是新节点)

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会进行这样的操作:

分析下整体流程:

  1. 比较A,A,相同类型的节点,进行patch,但数据相同,不发生dom操作
  2. 比较B,B,相同类型的节点,进行patch,但数据相同,不发生dom操作
  3. 比较C,F,相同类型的节点,进行patch,数据不同,发生dom操作
  4. 比较D,C,相同类型的节点,进行patch,数据不同,发生dom操作
  5. 比较E,D,相同类型的节点,进行patch,数据不同,发生dom操作
  6. 循环结束,将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:

四、Vue 2 与 Vue3 中的区别

在 Vue 2 中:

在 Vue3 中:

区块树(Block Tree) 是Vue 3中引入的一种重要的优化机制,它是Vue 3编译器在解析模板时生成的一种树状结构,它基于模板中的代码逻辑将模板划分为多个区块(Block)。

区块树的主要功能是在渲染时优化DOM操作,它可以将模板中的静态内容和动态内容区分开,从而在更新时只关注那些实际发生变化的动态内容。 通过减少不必要的渲染开销来提高性能。

综上:

举例:

<!-- 模板 -->
<div>
  <span>Static</span>         <!-- 静态提升(Hoist Static) -->
  <span :class="cls"></span>  <!-- 标记动态节点类型(PatchFlags ) class -->
  <span>{{ text }}</span>     <!-- 标记动态节点类型(PatchFlags ) text-->
</div>

这种机制使得 Vue 3 在具有大量静态内容+局部动态更新的场景下,性能优势尤为明显。

不建议用index作为key

不建议 用 index 作为 key,和没写基本上没区别,因为不管你数组的顺序怎么颠倒,index 都是 0, 1, 2 这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作,效率底。

建议key用唯一标识,如:id、手机号、学号等

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文