虚拟 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 来尽量减少节点的重新创建和删除
作者:工边页字
链接:https://juejin.cn/post/7113124543136268295
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。