React原理剖析之diff算法,一点也不难!!!

1,835 阅读6分钟

本人已参与「新人创作礼」活动,一起开启掘金创作之路。

diff算法听起来很深奥。其实真的没那么难。今天我们就用最简单的话把diff算法讲清楚。

虚拟Dom

什么是虚拟Dom

在传统的dom中只有真实Dom↓

image.png

这玩意就是真实DOM,我们能看见的节点元素。↓

image.png

react/vue这些组件化的框架诞生后,增加了一个概念,虚拟Dom。所以,在传统的浏览器和真实Dom之间增加了一层。虚拟Dom层。成了这样↓

image.png

虚拟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的情况下浏览器会这么做↓

  1. 删除所有节点
  2. 重新创建div标签,给div标签赋予class
  3. 在div下重新创建p标签,给P标签增加class并填充文本
  4. 在div下重新创建h1标签并填充文本

观察三个步骤,其中最耗费性能的,就是创建节点的过程。

在使用虚拟DOM的情况下。更新视图会有如下操作↓

  1. 比对结构一和结构二的树结构
  2. 发现div标签没有变。直接拿来复用(这样就不用删除也不用重新创建了)
  3. 发现p标签也没有变动,可以直接拿来复用(不用重新删除并创建)
  4. 发现div标签变成了h1标签,删除原先div标签,重新创建h1标签并插入(只有这一个需要重新生成)

然后就明朗了。

在不使用虚拟DOM的情况下,哪怕改变的只有一个子节点,所有的节点也都需要重新渲染。(我们以前使用JQ的那个时代,就是这么干的)

在使用虚拟DOM的情况下。有很多没有改变的节点(或组件)可以被复用, 直接省掉了好多创建节点的步骤。所以虚拟DOM会带来性能上的提升。

diff算法

很简单,在虚拟DOM中,会有两棵树,一颗修改前的树,一颗修改后的树。

在react中,diff中有三大策略。

diff三大策略

  1. Tree diff (树比对)
    • 比较新旧两棵树,找出哪些节点需要更新
    • 发现组件则进入 Component diff
    • 发现节点则进入 Element diff
  2. Component diff (组件比对)
    • 如果节点是组件,比对组件类型
    • 类型不同直接替换(删除旧组件,创建新组件)
    • 类型相同则只更新属性
    • 然后深入组件进行 Tree diff(递归)
  3. Element diff (元素比对)
    • 如果节点是原生标签,则看标签名

    • 标签名不同直接替换(删除旧元素,创建新元素)

    • 标签名相同则只更新属性

    • 然后深入标签做 Tree diff(递归)

比对会从Tree diff(树比对开始),扫到组件进入Component diff,扫到元素则进入Elemenet diff。这是一个递归查询的过程。

key是干啥的?这玩意是如何对项目进行优化的。

首先,diff会对新旧两颗树进行同级比较。绝不会出现跨层比较的情况。

image.png

在某一层比对发现元素类型不一样之后,自该层往下节点会直接全部删除,不会再往下比对。

更有意思的是↓

image.png

当组件树发生上图这样的变化时,从一个人类的思维来说,应该是删除p元素,再把span元素移动到左边。

但机器不是这么理解的!!

在机器的理解上,diff算法会对以上例子做出以下动作

  • 比对新旧节点数第一层的div是否被更改,没有改动,进入下一层比较
  • 发现旧节点树下第一个元素是p。新节点树下的第一个节点是span (p 比 span)
  • 删除p元素,创建span元素。
  • 再进行比对,发现旧节点树第二个元素是span,新节点树没有第二个元素(span 比 undefined)
  • 删除span元素

也就是说,如果你删除第一个元素,后面所有的元素都会被删除然后重新创建。

基于机器这种笨重的思维方式情况下。

key诞生了。

说白了。key是一种标记。

没有用key的情况下机器默认的算法是按顺序一个一个去比较(注意奥,是按元素顺序去比较)。只要对应顺序上的元素类型对不上,直接删除然后重建。

但是!!用了key则不一样。用了key机器会开启另一种比较算法。

当虚拟dom发现一个节点存在key以后。他就不会按顺序去比较旧节点树中相同位置的那个元素。而是会在旧树的同级元素中遍历寻找该key所标记的节点是否存在。是否可以复用。

不使用key的情况 image.png

如上图所示,在不使用key的情况下。diff算法会根据元素的位置去比对。如果你像上图那样删除了第一个元素的话,后面的元素都会因为比对不成功而全部被删除然后重新创建。

在使用key的情况下

image.png

在使用了key的情况下,会寻找匹配新旧节点树中是否有可以复用的元素。这样可以避免不必要的性能浪费。

总结

  • 首先,虚拟dom本质上就只是一个js对象而已
  • diff算法有三大策略,树比对(Tree diff),组件比对(Component diff),元素比对(Element diff)
  • 尽量使用key,在增删元素的时候可以提高性能
  • 创建并渲染一个节点的流程相对耗费性能,所以需要虚拟dom来尽量减少节点的重新创建和删除