React Diff 分析
一 diff策略
React用 三大策略 将O(n^3)复杂度
转化为 O(n)复杂度
策略一(tree diff):
Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
策略二(component diff):
拥有相同类的两个组件 生成相似的树形结构,
拥有不同类的两个组件 生成不同的树形结构。
策略三(element diff):
对于同一层级的一组子节点,通过唯一id区分。
基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。
二 tree diff
(1)React通过updateDepth对Virtual DOM树进行层级控制
。
(2)对树分层比较,两棵树 只对同一层次节点
进行比较。如果该节点不存在时,则该节点及其子节点会被完全删除,不会再进一步比较。
(3)只需遍历一次,就能完成整棵DOM树的比较。
那么问题来了,如果DOM节点出现了跨层级操作,diff会咋办呢?
答:diff只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作
。
如上图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A
。
由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作, 可以通过CSS隐藏、显示节点,而不是真正地移除、添加DOM节点。
。
三 component diff
React对不同的组件间的比较,有三种策略
(1)同一类型的两个组件,按原策略(层级比较)继续比较Virtual DOM树即可。
(2)同一类型的两个组件,组件A变化为组件B时,可能Virtual DOM没有任何变化,如果知道这点(变换的过程中,Virtual DOM没有改变),可节省大量计算时间,所以 用户 可以通过 shouldComponentUpdate()
来判断是否需要 判断计算。
(3)不同类型的组件,将一个(将被改变的)组件判断为dirty component(脏组件),从而替换 整个组件的所有节点
。
如上图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构
,而是直接删除 component D,重新创建 component G 以及其子节点。虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。
四 element diff
当节点处于同一层级时,diff提供三种节点操作:INSERT_MARKUP(插入)
、MOVE_EXISTING(移动)
和 REMOVE_NODE(删除)
.
INSERT_MARKUP(插入)
新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
eg: 组件 C 不在集合(A,B)中,需要插入
REMOVE_NODE(删除)
(1)老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作
eg : 组件 D 在集合(A,B,D)中,但 D的节点已经更改,不能复用和更新,所以需要删除 旧的 D ,再创建新的。
(2)老 component 不在新集合里的,也需要执行删除操作
eg : 组件 D 之前在 集合(A,B,D)中,但集合变成新的集合(A,B)了,D 就需要被删除。
MOVE_EXISTING(移动)
旧集合中有新组件类型,且 element 是可更新的类型,generateComponentChildren 已调用receiveComponent ,这种情况下 prevChild=nextChild ,就需要做移动操作,可以复用以前的 DOM 节点。
情形一:新旧集合中存在相同节点但位置不同时,如何移动节点
老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。
React 发现这类操作繁琐冗余,因为这些都是相同的节点,但由于位置发生变化,导致需要进行繁杂低效的删除、创建操作,其实只要对这些节点进行位置移动即可
针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分
,虽然只是小小的改动,性能上却发生了翻天覆地的变化!
新老集合所包含的节点,如下图所示,新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。
具体的流程我们用一张表格来展现一下:
newIndex | 节点 | index | lastIndex | 操作 | |
---|---|---|---|---|---|
0 | B | 1 | 0 | index(1) > lastIndex(0),lastIndex=index | |
1 | A | 0 | 1 | index(0) < lastIndex(1),节点A移动至index(1)的位置 | |
2 | D | 3 | 1 | index(3) > lastIndex(1),lastIndex=index | |
3 | C | 2 | 3 | index(2) < lastIndex(3),节点C移动至index(2)的位置 |
- newIndex 新集合的遍历下标。
- index:当前节点在老集合中的下标。
- lastIndex:在新集合访问过的节点中,其在老集合的最大下标值。
注意:lastIndex有点像浮标,或者说是一个map的索引,一开始默认值是0,它会与map中的元素进行比较,比较完后,会改变自己的值的(取index和lastIndex的较大数)。
操作一栏中只比较index和lastIndex:
- 当index > lastIndex时,将index的值赋值给lastIndex
- 当index = lastIndex时,不操作
- 当index < lastIndex时,将当前节点移动到newIndex的位置
描述 diff 的差异对比过程如下:
-
(1)看着上图的 B,React先从新中取得B,然后判断旧中是否存在相同节点B,当发现存在节点B后,就去判断是否移动B。
B在旧 中的index=1,它的lastIndex=0,不满足index < lastIndex
的条件,因此 B 不做移动操作。此时,一个操作是,lastIndex=(index,lastIndex)中的较大数=1
. -
(2)看着 A,A在旧的index=0,此时的lastIndex=1(因为先前与新的B比较过了),满足index<lastIndex,因此,对A进行移动操作,此时lastIndex=max(index,lastIndex)=1。
-
(3)看着D,同(1),不移动,由于D在旧的index=3,比较时,lastIndex=1,所以改变
lastIndex=max(index,lastIndex)=3
-
(4)看着C,同(2),移动,C在旧的index=2,满足index<lastIndex(lastIndex=3),所以移动。
-
由于C已经是最后一个节点,所以diff操作结束。
情形二:新集合中有新加入的节点,旧集合中有删除的节点
具体的流程我们用一张表格来展现一下:
newIndex | 节点 | index | lastIndex | 操作 |
---|---|---|---|---|
0 | B | 1 | 0 | index(1) > lastIndex(0),lastIndex=index(1) |
1 | E | - | 1 | index不存在,添加节点E至newIndex(1)的位置 |
2 | C | 2 | 1 | 不操作 |
3 | A | 0 | 3 | index(0) < lastIndex(3),节点A移动至newIndex(3)的位置 |
同样操作一栏中只比较index和lastIndex,但是index可能有不存在的情况:
- index存在
- 当index > lastIndex时,将index的值赋值给lastIndex
- 当index = lastIndex时,不操作
- 当index < lastIndex时,将当前节点移动到newIndex的位置
- index不存在
- 新增当前节点至newIndex的位置
描述 diff 的差异对比过程如下:
- (1)B不移动,index=1,它的lastIndex=0,不满足
index < lastIndex
的条件,因此 B 不做移动操作,更新lastIndex=(index,lastIndex)中的较大数=1 - (2)新集合取得 E,发现旧不存在,故在lastIndex=1的位置 创建E,更新lastIndex=1
- (3)新集合取得C,C不移动,更新lastIndex=2
- (4)新集合取得A,A移动,同上,更新lastIndex=2
- (5)新集合对比后,再对旧集合遍历。判断 新集合 没有,但 旧集合 有的元素(如D,新集合没有,旧集合有),发现 D,删除D,diff操作结束。
diff的不足与待优化的地方
若新集合的节点更新为:D、A、B、C,与老集合对比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在老集合的位置是最大的,D不移动,但它的index是最大的,导致更新lastIndex=3,从而使得其他元素A,B,C的index<lastIndex,导致A,B,C都要去移动。
建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
总结
-
React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
-
React 通过
分层求异
的策略,对 tree diff 进行算法优化; -
React 通过
相同类生成相似树形结构,不同类生成不同树形结构
的策略,对 component diff 进行算法优化; -
React 通过
设置唯一 key
的策略,对 element diff 进行算法优化; -
建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
-
建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。