本人已参与「新人创作礼」活动,一起开启掘金创作之路。
diff算法听起来很深奥。其实真的没那么难。今天我们就用最简单的话把diff算法讲清楚。
虚拟Dom
什么是虚拟Dom
在传统的dom中只有真实Dom↓
这玩意就是真实DOM,我们能看见的节点元素。↓
react/vue这些组件化的框架诞生后,增加了一个概念,虚拟Dom。所以,在传统的浏览器和真实Dom之间增加了一层。虚拟Dom层。成了这样↓
虚拟DOM本质上他就只是一个对象!!! 没什么难的,虚拟DOM就只是一个对象而已。只是这玩意,我们不能在视图上只管的看见,它在源码里。
我们拿一段真实DOM来映射一下与之对应的虚拟Dom↓
//真实DOM
<div class="d-class" key="d">
<p class="p-class">我是p元素</p>
</div>
//与之对应的虚拟DOM
const vNode = {
key:"d", //是否有key,有则显示,无则显示null
props:{
//标签里是否子元素
children:[
{type:'p',....},
],
onClick:() => {}, //标签上的事件
className:"d-class", //标签上的属性
}
ref:"null",
type:"div"
}
vNode这种对象,这玩意就是react中虚拟DOM的真实面目。
为什么要有虚拟DOM
因为重新从头生成个正正经经的DOM节点开销实在是太大了!!! 从浏览器开始创建一个节点到这个节点完全渲染到视图上去,是比较耗费性能的,当一群节点都需要去创建并渲染的时候,极端情况下,会出现肉眼可见的卡顿。虚拟Dom的作用就是尽量减少元素的创建。
我们再来一个简单的流程解释一下
// dom结构 一
<div class="d-class">
<p class="p-class">我是p元素</p>
<div>后面需要被改变的标签<div>
</div>
// dom结构 二
<div class="d-class">
<p class="p-class">我是p元素</p>
<h1>节点被更改<h1>
</div>
观察上面的dom节点一和dom节点二,不难发现。其实只有div改变成了h1。但就是这一个小小的改动,浏览器重新渲染的话,不用虚拟dom的情况下浏览器会这么做↓
- 删除所有节点
- 重新创建div标签,给div标签赋予class
- 在div下重新创建p标签,给P标签增加class并填充文本
- 在div下重新创建h1标签并填充文本
观察三个步骤,其中最耗费性能的,就是创建节点的过程。
在使用虚拟DOM的情况下。更新视图会有如下操作↓
- 比对结构一和结构二的树结构
- 发现div标签没有变。直接拿来复用(这样就不用删除也不用重新创建了)
- 发现p标签也没有变动,可以直接拿来复用(不用重新删除并创建)
- 发现div标签变成了h1标签,删除原先div标签,重新创建h1标签并插入(只有这一个需要重新生成)
然后就明朗了。
在不使用虚拟DOM的情况下,哪怕改变的只有一个子节点,所有的节点也都需要重新渲染。(我们以前使用JQ的那个时代,就是这么干的)
在使用虚拟DOM的情况下。有很多没有改变的节点(或组件)可以被复用, 直接省掉了好多创建节点的步骤。所以虚拟DOM会带来性能上的提升。
diff算法
很简单,在虚拟DOM中,会有两棵树,一颗修改前的树,一颗修改后的树。
在react中,diff中有三大策略。
diff三大策略
- Tree diff (树比对)
- 比较新旧两棵树,找出哪些节点需要更新
- 发现组件则进入 Component diff
- 发现节点则进入 Element diff
- Component diff (组件比对)
- 如果节点是组件,比对组件类型
- 类型不同直接替换(删除旧组件,创建新组件)
- 类型相同则只更新属性
- 然后深入组件进行 Tree diff(递归)
- Element diff (元素比对)
-
如果节点是原生标签,则看标签名
-
标签名不同直接替换(删除旧元素,创建新元素)
-
标签名相同则只更新属性
-
然后深入标签做 Tree diff(递归)
-
比对会从Tree diff(树比对开始),扫到组件进入Component diff,扫到元素则进入Elemenet diff。这是一个递归查询的过程。
key是干啥的?这玩意是如何对项目进行优化的。
首先,diff会对新旧两颗树进行同级比较。绝不会出现跨层比较的情况。
在某一层比对发现元素类型不一样之后,自该层往下节点会直接全部删除,不会再往下比对。
更有意思的是↓
当组件树发生上图这样的变化时,从一个人类的思维来说,应该是删除p元素,再把span元素移动到左边。
但机器不是这么理解的!!
在机器的理解上,diff算法会对以上例子做出以下动作
- 比对新旧节点数第一层的div是否被更改,没有改动,进入下一层比较
- 发现旧节点树下第一个元素是p。新节点树下的第一个节点是span (p 比 span)
- 删除p元素,创建span元素。
- 再进行比对,发现旧节点树第二个元素是span,新节点树没有第二个元素(span 比 undefined)
- 删除span元素
也就是说,如果你删除第一个元素,后面所有的元素都会被删除然后重新创建。
基于机器这种笨重的思维方式情况下。
key诞生了。
说白了。key是一种标记。
没有用key的情况下机器默认的算法是按顺序一个一个去比较(注意奥,是按元素顺序去比较)。只要对应顺序上的元素类型对不上,直接删除然后重建。
但是!!用了key则不一样。用了key机器会开启另一种比较算法。
当虚拟dom发现一个节点存在key以后。他就不会按顺序去比较旧节点树中相同位置的那个元素。而是会在旧树的同级元素中遍历寻找该key所标记的节点是否存在。是否可以复用。
不使用key的情况
如上图所示,在不使用key的情况下。diff算法会根据元素的位置去比对。如果你像上图那样删除了第一个元素的话,后面的元素都会因为比对不成功而全部被删除然后重新创建。
在使用key的情况下
在使用了key的情况下,会寻找匹配新旧节点树中是否有可以复用的元素。这样可以避免不必要的性能浪费。
总结
- 首先,虚拟dom本质上就只是一个js对象而已
- diff算法有三大策略,树比对(Tree diff),组件比对(Component diff),元素比对(Element diff)
- 尽量使用key,在增删元素的时候可以提高性能
- 创建并渲染一个节点的流程相对耗费性能,所以需要虚拟dom来尽量减少节点的重新创建和删除