在面试时候面试官问我请说说vue 中的diff是什么?半罐响叮当的我哪能清楚那么多原理,吞吞吐吐说了几分钟,当然最后这个面试肯定是黄了。不过秉着缺啥补啥,还得调整心态重新再来,所有这篇文档将讲讲现在的我对应这个vue diff这个知识点理解。
项目案例
先看看两个在开发过程中容易被忽略的bug,先看下面的效果:
效果图1
在效果图1中:第一个列表添加元素可以看到F12元素全部
li做了更新变动,在第二个案例做页面切换可以看到内容input还是之前内容,没有刷新。
效果图2
在效果图2中:第一个列表添加元素可以看到F12元素只有第一个li更新变动,在第二个案例做页面切换可以看到内容input跟随页面变化清空。
像上列在开发中是两种是比较常见问题,第一个列表添加元素是性能加载问题,实际可能不影响功能有些人会忽略掉,但假设如果不是简单的li而是每个li有很多元素,这样每添加一个元素都将li所有元素都重新加载那就出现大性能问题了。同样假设第二个案例,点击开关切换页面,却还保留着之前输入的内容,也不符合习惯和常理。
其实这些问题都与这次讲vue的diff算法有深入的关系,当了解diff是干啥的,那么自然会理解刚才案例中为何出现这个问题,自如知道应该如何解决。
什么是diff
- 当组件创建和更新时,vue均会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom
- 对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程
- 在对比时,vue采用深度优先、同层比较的方式进行比对。
- 在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的
- 具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。
- 在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。
- 这样一直递归的遍历下去,直到整棵树完成对比。
在控制台中输入可以看到两个函数_update、_vnode,diff算法主要是执行这两个函数。
接下来通过伪代码讲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);
// ... 其他代码
}
可以看这篇文档,他按照每个节点运行用图的形式表示处理。【vue】diff 算法详解_diff算法_疾风小蜗牛的博客-CSDN博客
patch函数
patch函数是Vue中用于执行diff算法的核心函数,它用于比较新旧虚拟DOM树的差异,并将更新部分应用到实际的DOM上。下面是patch函数的大致对比流程:
- 首先,patch函数会比较新旧节点的类型,包括元素节点、文本节点等。如果类型不同,直接用新节点替换旧节点,并更新到实际的DOM上。
- 如果类型相同,patch函数会进一步比较节点的属性和事件监听器。如果有差异,更新属性和事件监听器。
- 接下来,patch函数会比较节点的子节点。如果新节点没有子节点,而旧节点有,则移除旧节点的子节点。反之,如果旧节点没有子节点,而新节点有,则添加新节点的子节点。
- 如果新旧节点都有子节点,则patch函数会进行更详细的比较。它会使用一种称为“双端比较”的策略,即从新旧节点的首尾开始比较子节点。
- 在比较子节点时,patch函数会根据一定的规则判断是否是同一个节点。如果是同一个节点,则递归调用patch函数进行进一步的比较和更新。如果不是同一个节点,则直接将新节点插入到旧节点前面,并更新到实际的DOM上。
- 在比较完所有子节点后,如果新节点的子节点数量大于旧节点的子节点数量,则将多出来的节点添加到实际的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>
案例一
第一个列表添加元素
<ul class="demo01">
<li v-for="item in data1" >
{{item}}
</li>
</ul>
根据上面代码,li进行循环渲染<li>{{item}}</li>,由于循环每个li的节点的类型和元素节点都一样,所以当新旧两个树进行对比的时候,如下图,将每个节点都进行修改(例如1修改为1700576452021),然后每个都做重复更新修改,出现上面动态中所有节点发生改变
修改过后
<ul class="demo01">
<li v-for="item in data1" :key="item">
{{item}}
</li>
</ul>
<li :key="item"> {{item}} </li>在li添加key这样在元素节点上做了区分,执行diff算法时候会按照下面图方式,找到对应的旧的节点,然后保留只更新新的节点,实现最新更新视图效果。
案例二
<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>
同样的可以看出两个input他的节点的类型和元素节点是一样的,当页面切换的时候根据diff算法会将input算为一个元素,未发生变化。我们给他添加key做区分,这样inputVue会识别为两个不同的input,这样切换页面就会移除旧的input元素在添加input元素。
总结
在vue中diff算法做了什么事情
- 当组件创建和更新时,vue均会执行内部的update函数,该函数使用render函数生成的虚拟dom树,将新旧两树进行对比,找到差异点,最终更新到真实dom
- 对比差异的过程叫diff,vue在内部通过一个叫patch的函数完成该过程
- 在对比时,vue采用深度优先、同层比较的方式进行比对。
- 在判断两个节点是否相同时,vue是通过虚拟节点的key和tag来进行判断的
- 具体来说,首先对根节点进行对比,如果相同则将旧节点关联的真实dom的引用挂到新节点上,然后根据需要更新属性到真实dom,然后再对比其子节点数组;如果不相同,则按照新节点的信息递归创建所有真实dom,同时挂到对应虚拟节点上,然后移除掉旧的dom。
- 在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。
- 这样一直递归的遍历下去,直到整棵树完成对比。