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)的位置
> 注:最后还需要对旧集合进行循环遍历,找出新集合中没有的节点,此时发现存在这样的节点D,因此删除节点D,到此 diff 操作全部完成。

同样操作一栏中只比较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 的渲染性能。

posted @ 2020-10-30 10:38  qiqi715  阅读(489)  评论(0编辑  收藏  举报