探究React协调中的diff思想

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」。

1、什么是协调

协调就是通过如 ReactDOM 等类库使虚拟DOM与真实的DOM同步。通俗的讲协调是将虚拟DOM映射到真实DOM的过程。而diff是协调的一个环节,协调是使一致的过程,而Diff是找不同的过程。

2、diff设计思想

在传统方式中,要找出两棵树不同,需要一一对节点对比,这个过程的算法复杂度是O(n³)。也就是说假如一棵树有100个节点,那么比较一次就需要操作10w次,代价是非常昂贵的。那么是如何简化这个过程的呢?就是将 O (n³) 复杂度转换成 O (n) 复杂度:

  • 若两个组件属于同一个类型,那么它们将拥有相同的 DOM 树形结构;
  • 处于同一层级的一组子节点,可用通过设置 key 作为唯一标识,从而维持各个节点在不同渲染过程中的稳定性。

diff逻辑:

  • diff 算法性能突破的关键点在于“分层对比”;
  • 类型相同的节点才有继续 diff 的必要性;
  • 设置key 属性,重用同一层级内的节点。

2.1 分层对比

它只针对相同层级的节点作对比,如下图所示。

分层比较.jpg

销毁 + 重建的代价是昂贵的,尽量保持 DOM 结构的稳定性。所以React发生了跨层级的节点操作,它只能认为移出子树那一层的组件消失了,对应子树需要被销毁;而移入子树的那一层新增了一个组件,需要重新为其创建一棵子树。

1231.jpg

2.2 节点类型判断

在React 中,只有同类型的组件,才有进一步对比的必要性;若参与 Diff 的两个组件类型不同,那么直接放弃比较,原地替换掉旧的节点。只有确认组件类型相同后,React 才会在保留组件对应 DOM 树(或子树)的基础上,尝试向更深层次去 Diff。这样一来,便能够从很大程度上减少 Diff 过程中冗余的递归操作。如下图所示,直接移除span以及后代所有节点,新增p节点及后代节点;

重构节点类型.jpg

2.3 使用key来保持节点的稳定性

key属性的设置,可以帮我们尽可能重用同一层级内的节点。它就像一个记号一样,帮助记住某一个节点,从而在后续的更新中实现对节点的追踪;

key 是用来帮助 React 识别哪些内容被更改、添加或者删除。key 需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果 key 值发生了变更,React 则会触发 UI 的重渲染。这是一个非常有用的特性。

key.jpg

这个 key 就是每个节点的唯一标识,有了这个标识之后,当 C 被插入到 B 和 D 之间时,React 并不会再认为 C、D、E 这三个坑位都需要被重建——它会通过识别唯一标识,意识到 D 和 E 并没有发生变化(D 的 ID 仍然是 1,E 的 ID 仍然是 2),而只是被调整了顺序而已。接着,React 便能够轻松地重用它追踪到旧的节点,将 D 和 E 转移到新的位置,并完成对 C 的插入。这样一来,同层级下元素的操作成本便大大降低。

3、diff算法

先来看一下节点复用的条件,只有key和type都相同,才能复用节点;

节点复用.jpg

3.1 单节点diff

新旧节点 key 和 type 有一个不同就不能复用;

// old DOM
<div>
  <h1 key="h1">h1</h1> // 需要把老节点标记为删除
</div>

// -----------------------------

// new DOM
<div>
  <h2 key="h2">h2</h2> // 生成新的fiber节点并标记为插入
</div>
复制代码

从上面代码可以看出,old DOM和new DOM的type和key都不相同,所以不能复用;

3.2 多节点diff

节点存在更新、删除、新增操作;

移动的原则是尽量少的移动,如过必须要动,新地位高的不动,低的动;

会经历两轮遍历:

  • 一轮主要节点更新,属性和类型;
  • 二轮主要处理新增、删除和移动;

情况一:

key 相同,type 相同,顺序相同,更新节点;

// old DOM
<ul>
    <li key="A">A</li>   
    <li key="B">B</li>    
    <li key="C">C</li>		
    <li key="D">D</li>	
  </ul>

//-------------------------------

// new DOM
<ul>
   <li key="A">new A</li>
   <li key="B">new B</li>
   <li key="C">new C</li>
   <li key="D">new D</li>
 </ul>
复制代码

操作步骤:

  • 更新A
  • 更新B
  • 更新C
  • 更新D

情况二:

key 相同,type 不同,顺序相同,删除老的,添加新的;

// old DOM
<ul>
   <li key="A">A</li>
   <li key="B">B</li>
   <li key="C">C</li>
   <li key="D">D</li>
</ul>

// -------------------------------

// new DOM
<ul>
   <div key="A">new A</div>
   <li key="B">new B</li>
   <li key="C">new C</li>
   <li key="D">new D</li>
 </ul>
复制代码

操作步骤:

  • 删除老的 li A
  • 插入新的div A
  • 更新 B 、更新 C、 更新 D

情况三:

key相同,type相同,顺序不同;

// old DOM
<ul>
  <li key="A">A</li>
  <li key="B">B</li>
  <li key="C">C</li>
  <li key="D">D</li>
  <li key="F">F</li>
</ul>


// -------------------------------
      
// new DOM
<ul>
  <li key="A">new A</li>
  <li key="C">new C</li>
  <li key="D">new D</li>
  <li key="B">new B</li>
  <li key="E">new E</li>    
</ul>
复制代码
  1. 进行一轮新节点遍历
  2. old A节点和new A节点对比,发现一样,更新A;
  3. 当遍历到new C发现key不一样,则立即跳出第一轮循环,key不一样可能有位置变化;
  4. 开启第二轮循环:建立一个Map,Map的key就是元素的key,值就是老的fiber节点
let map = {B:B,C:C,D:D,F:F}  // 疑问为啥构建Map?  解答:为了方便查找
复制代码
  1. 继续遍历新节点;
  2. 拿new C节点去Map中找,看看有没有,如果有,说明是位置变了,交换位置,更新节点,老节点可以复用(fiber和dom可以复用),如果没有就标记为新增,遍历结束Map中剩下的标记为删除;

交换节点算法如图所示:

diff算法.png

  1. new C节点对应old C节点的oldIndex为2,大于lastPlacedIndex(2>0),所以old C节点不动,更新lastPlacedIndex = 2
  2. new D节点对应old D节点的oldIndex为3,大于lastPlacedIndex(3>2),所以old D节点不动,更新lastPlacedIndex = 3,
  3. new B节点对应old B节点的oldIndex为1,大于lastPlacedIndex(1<3),所以old B节点移动,更新lastPlacedIndex = 3
  4. E节点标记为新增
  5. F节点标记为删除

4、小结

本文通过对diff思想以及diff算法做了详细讲解,希望读者能够认真并且认真读懂其中的原理,相信会对吃透React源码有很大的帮助!

おすすめ

転載: juejin.im/post/7054725886855086087