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 有一个不同就不能复用;

  1. // old DOM
  2. <div>
  3. <h1 key="h1">h1</h1> // 需要把老节点标记为删除
  4. </div>
  5. // -----------------------------
  6. // new DOM
  7. <div>
  8. <h2 key="h2">h2</h2> // 生成新的fiber节点并标记为插入
  9. </div>

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

3.2 多节点diff

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

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

会经历两轮遍历:

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

情况一:

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

  1. // old DOM
  2. <ul>
  3. <li key="A">A</li>
  4. <li key="B">B</li>
  5. <li key="C">C</li>
  6. <li key="D">D</li>
  7. </ul>
  8. //-------------------------------
  9. // new DOM
  10. <ul>
  11. <li key="A">new A</li>
  12. <li key="B">new B</li>
  13. <li key="C">new C</li>
  14. <li key="D">new D</li>
  15. </ul>

操作步骤:

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

情况二:

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

  1. // old DOM
  2. <ul>
  3. <li key="A">A</li>
  4. <li key="B">B</li>
  5. <li key="C">C</li>
  6. <li key="D">D</li>
  7. </ul>
  8. // -------------------------------
  9. // new DOM
  10. <ul>
  11. <div key="A">new A</div>
  12. <li key="B">new B</li>
  13. <li key="C">new C</li>
  14. <li key="D">new D</li>
  15. </ul>

操作步骤:

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

情况三:

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

  1. // old DOM
  2. <ul>
  3. <li key="A">A</li>
  4. <li key="B">B</li>
  5. <li key="C">C</li>
  6. <li key="D">D</li>
  7. <li key="F">F</li>
  8. </ul>
  9. // -------------------------------
  10. // new DOM
  11. <ul>
  12. <li key="A">new A</li>
  13. <li key="C">new C</li>
  14. <li key="D">new D</li>
  15. <li key="B">new B</li>
  16. <li key="E">new E</li>
  17. </ul>
  1. 进行一轮新节点遍历
  2. old A节点和new A节点对比,发现一样,更新A;
  3. 当遍历到new C发现key不一样,则立即跳出第一轮循环,key不一样可能有位置变化;
  4. 开启第二轮循环:建立一个Map,Map的key就是元素的key,值就是老的fiber节点

    1. let map = {B:B,C:C,D:D,F:F} // 疑问为啥构建Map? 解答:为了方便查找
  5. 继续遍历新节点;

  6. 拿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源码有很大的帮助!