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 分层对比
它只针对相同层级的节点作对比,如下图所示。
销毁 + 重建的代价是昂贵的,尽量保持 DOM 结构的稳定性。所以React发生了跨层级的节点操作,它只能认为移出子树那一层的组件消失了,对应子树需要被销毁;而移入子树的那一层新增了一个组件,需要重新为其创建一棵子树。
2.2 节点类型判断
在React 中,只有同类型的组件,才有进一步对比的必要性;若参与 Diff 的两个组件类型不同,那么直接放弃比较,原地替换掉旧的节点。只有确认组件类型相同后,React 才会在保留组件对应 DOM 树(或子树)的基础上,尝试向更深层次去 Diff。这样一来,便能够从很大程度上减少 Diff 过程中冗余的递归操作。如下图所示,直接移除span以及后代所有节点,新增p节点及后代节点;
2.3 使用key来保持节点的稳定性
key属性的设置,可以帮我们尽可能重用同一层级内的节点。它就像一个记号一样,帮助记住某一个节点,从而在后续的更新中实现对节点的追踪;
key 是用来帮助 React 识别哪些内容被更改、添加或者删除。key 需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果 key 值发生了变更,React 则会触发 UI 的重渲染。这是一个非常有用的特性。
这个 key 就是每个节点的唯一标识,有了这个标识之后,当 C 被插入到 B 和 D 之间时,React 并不会再认为 C、D、E 这三个坑位都需要被重建——它会通过识别唯一标识,意识到 D 和 E 并没有发生变化(D 的 ID 仍然是 1,E 的 ID 仍然是 2),而只是被调整了顺序而已。接着,React 便能够轻松地重用它追踪到旧的节点,将 D 和 E 转移到新的位置,并完成对 C 的插入。这样一来,同层级下元素的操作成本便大大降低。
3、diff算法
先来看一下节点复用的条件,只有key和type都相同,才能复用节点;
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>
- 进行一轮新节点遍历
- old A节点和new A节点对比,发现一样,更新A;
- 当遍历到new C发现key不一样,则立即跳出第一轮循环,key不一样可能有位置变化;
开启第二轮循环:建立一个Map,Map的key就是元素的key,值就是老的fiber节点
let map = {B:B,C:C,D:D,F:F} // 疑问为啥构建Map? 解答:为了方便查找
继续遍历新节点;
- 拿new C节点去Map中找,看看有没有,如果有,说明是位置变了,交换位置,更新节点,老节点可以复用(fiber和dom可以复用),如果没有就标记为新增,遍历结束Map中剩下的标记为删除;
交换节点算法如图所示:
- new C节点对应old C节点的oldIndex为2,大于lastPlacedIndex(2>0),所以old C节点不动,更新lastPlacedIndex = 2
- new D节点对应old D节点的oldIndex为3,大于lastPlacedIndex(3>2),所以old D节点不动,更新lastPlacedIndex = 3,
- new B节点对应old B节点的oldIndex为1,大于lastPlacedIndex(1<3),所以old B节点移动,更新lastPlacedIndex = 3
- E节点标记为新增
- F节点标记为删除
4、小结
本文通过对diff思想以及diff算法做了详细讲解,希望读者能够认真并且认真读懂其中的原理,相信会对吃透React源码有很大的帮助!