vue的diff算法

605 阅读8分钟

在面试时候面试官问我请说说vue 中的diff是什么?半罐响叮当的我哪能清楚那么多原理,吞吞吐吐说了几分钟,当然最后这个面试肯定是黄了。不过秉着缺啥补啥,还得调整心态重新再来,所有这篇文档将讲讲现在的我对应这个vue diff这个知识点理解。

项目案例

先看看两个在开发过程中容易被忽略的bug,先看下面的效果:

效果图1

0039.gif 在效果图1中:第一个列表添加元素可以看到F12元素全部 li做了更新变动,在第二个案例做页面切换可以看到内容input还是之前内容,没有刷新

效果图2

0040.gif

在效果图2中:第一个列表添加元素可以看到F12元素只有第一个li更新变动,在第二个案例做页面切换可以看到内容input跟随页面变化清空

像上列在开发中是两种是比较常见问题,第一个列表添加元素是性能加载问题,实际可能不影响功能有些人会忽略掉,但假设如果不是简单的li而是每个li有很多元素,这样每添加一个元素都将li所有元素都重新加载那就出现大性能问题了。同样假设第二个案例,点击开关切换页面,却还保留着之前输入的内容,也不符合习惯和常理。

其实这些问题都与这次讲vue的diff算法有深入的关系,当了解diff是干啥的,那么自然会理解刚才案例中为何出现这个问题,自如知道应该如何解决。

什么是diff

  1. 当组件创建和更新时,vue均会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom
  2. 对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程
  3. 在对比时,vue采用深度优先、同层比较的方式进行比对。
  4. 在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的
  5. 具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。
  6. 在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。
  7. 这样一直递归的遍历下去,直到整棵树完成对比。

在控制台中输入可以看到两个函数_update_vnode,diff算法主要是执行这两个函数。

image.png

image.png

接下来通过伪代码讲diff算法这两个函数是如何执行的。

// vue构造函数
function Vue(){
  // ... 其他代码
  var updateComponent = () => {
    //update函数传入新的dom树
    this._update(this._render()){
        //用一个变量保存旧的vnode节点
        let oldvnode = this.vnode;
        //将现在的vue指向最新的虚拟dom,新的vnode节点
         this.vnode = this._render();
        
        //比较新的虚拟dom和旧虚拟dom节点
        patch()
        //根据虚拟dom不同点,更新真实的dom
    }
  }
  //监听值变化
  new Watcher(updateComponent);
  // ... 其他代码
}

diff流程.drawio.png 可以看这篇文档,他按照每个节点运行用图的形式表示处理。【vue】diff 算法详解_diff算法_疾风小蜗牛的博客-CSDN博客

patch函数

patch函数是Vue中用于执行diff算法的核心函数,它用于比较新旧虚拟DOM树的差异,并将更新部分应用到实际的DOM上。下面是patch函数的大致对比流程:

  1. 首先,patch函数会比较新旧节点的类型,包括元素节点、文本节点等。如果类型不同,直接用新节点替换旧节点,并更新到实际的DOM上。
  2. 如果类型相同,patch函数会进一步比较节点的属性事件监听器。如果有差异,更新属性和事件监听器。
  3. 接下来,patch函数会比较节点的子节点。如果新节点没有子节点,而旧节点有,则移除旧节点的子节点。反之,如果旧节点没有子节点,而新节点有,则添加新节点的子节点。
  4. 如果新旧节点都有子节点,则patch函数会进行更详细的比较。它会使用一种称为“双端比较”的策略,即从新旧节点的首尾开始比较子节点。
  5. 在比较子节点时,patch函数会根据一定的规则判断是否是同一个节点。如果是同一个节点,则递归调用patch函数进行进一步的比较和更新。如果不是同一个节点,则直接将新节点插入到旧节点前面,并更新到实际的DOM上。
  6. 在比较完所有子节点后,如果新节点的子节点数量大于旧节点的子节点数量,则将多出来的节点添加到实际的DOM中。反之,如果旧节点的子节点数量大于新节点的子节点数量,则将多余的节点从实际的DOM中移除。

总结起来,patch函数会递归地比较新旧虚拟DOM树的差异,并根据差异进行相应的更新操作。它会比较节点类型、属性、事件监听器以及子节点,并根据一定的规则进行插入、替换和移除操作,最终将更新结果反映到实际的DOM上。这个对比流程是为了尽可能减少DOM操作,提高页面更新的效率。

回到最开始问题,先放代码index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        #app{
            display: flex;

        }
        .demo01{
            list-style:none;
            padding:0;
            margin:0;
        
        }
        .sorttable {
            border: 1px solid #000;
            width: 500px;
            text-align: center;
            height: 200px;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
        }
        .btn{
            position: relative;
            bottom: 0px;
        }
    </style>
</head>
<body>
    <div id="app">
        <div class="sorttable">
            <ul class="demo01">
                <li v-for="item in data1" :key="item">
                    {{item}}
                </li>
            </ul>
            <div class="btn">
                <button @click="sortdata()">排序</button>
                <button @click="insert()">插入</button>
            </div>
        </div>
        <div style="border: 1px solid #000; width: 500px;">
            <div style="border-bottom: #000 1px solid;">
                <input type="radio" value="1"   v-model="radio" checked/> 开
                <input type="radio"  value="2" v-model="radio" /> 关
            </div>
            <div v-if="radio==1">
                第一个页面 <input type="text" />
            </div>
            <div v-else>
                第二个页面 <input type="text"/>

            </div>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>  
        var app = new Vue({
            el:'#app',
            data:{
                temp:"hallo world",
                data1:[1,2,3,4,5],
                radio:"1",
            },
            mounted(){
                console.log(this.data1);
            },
            methods:{
                sortdata(){
                    console.log(this.data1);
                    this.data1.reverse();
                },
                insert(){
                    // this.data1.push(6);
                    this.data1 = [new Date().getTime()].concat(this.data1);
                }
            }
        });
    </script>
</body>
</html>

案例一

第一个列表添加元素

0041.gif
            <ul class="demo01">
                <li v-for="item in data1" >
                    {{item}}
                </li>
            </ul>

根据上面代码,li进行循环渲染<li>{{item}}</li>,由于循环每个li的节点的类型元素节点都一样,所以当新旧两个树进行对比的时候,如下图,将每个节点都进行修改(例如1修改为1700576452021),然后每个都做重复更新修改,出现上面动态中所有节点发生改变 image.png

修改过后

0043.gif

            <ul class="demo01">
                <li v-for="item in data1" :key="item">
                    {{item}}
                </li>
            </ul>

<li :key="item"> {{item}} </li>li添加key这样在元素节点上做了区分,执行diff算法时候会按照下面图方式,找到对应的旧的节点,然后保留只更新新的节点,实现最新更新视图效果。

image.png

案例二

            <div v-if="radio==1">
            <!--  第一个页面 <input type="text" :key="123"/> -->
                第一个页面 <input type="text" />
            </div>
            <div v-else>
            <!-- 第二个页面 <input type="text" :key="234"/> -->
                第二个页面 <input type="text" />
            </div>

0044.gif

同样的可以看出两个input他的节点的类型元素节点是一样的,当页面切换的时候根据diff算法会将input算为一个元素,未发生变化。我们给他添加key做区分,这样inputVue会识别为两个不同的input,这样切换页面就会移除旧的input元素在添加input元素。

总结

在vue中diff算法做了什么事情

  1. 当组件创建和更新时,vue均会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom
  2. 对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程
  3. 在对比时,vue采用深度优先、同层比较的方式进行比对。
  4. 在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的
  5. 具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。
  6. 在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。
  7. 这样一直递归的遍历下去,直到整棵树完成对比。