React diff算法

83 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情

本篇讲解diff算法基于React16之前

传统diff算法

通过循环递归进行对比,算法的时间复杂度达 O(n3)O(n^3)

React的diff算法

  • 什么是调和?
    将virtual DOM(虚拟dom)转换成actual DOM(真实dom)的 最少操作过程就叫调和,简单理解就是简化算法复杂度。
  • react的diff算法
    react的diff算法就是通过深度优先算法实现了上述的调和,简化了算法的复杂度。
  • diff算法实现基础
    1. 只比较同一层级的节点
    2. 如果两个节点类型不一样,以这两个节点为根节点的树会完全不同
    3. 开发者会用key标识出来多次render中结构保持不变的节点,以便重用

Diff策略

React通过tree diffcomponent diffelement diff三大策略将算法的时间复杂度复杂度从O(n3)O(n^3)降为O(n)O(n)

tree diff(树比较)

  1. React通过updateDepth对virtual DOM树进行层级控制
  2. 树分层进行比较,两棵树只对同一层的节点进行比较,如果节点不存在,则删除这个节点以及此节点下所有子节点,不会再进一步比较
  3. 只需遍历一次,就能完成整棵DOM树的比较

流程图2.jpg

如果遇到下面这样的情况:

流程图3.jpg

A为根节点的整棵树都会被重新创建,而不是移动。之前的以A为根节点的整棵树将会删除。

注:为了保持virtual DOM树的结构,

component diff(组件比较)

  • 如果是同一类型的组件,按照策略继续比较。
  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。

例如将下面结构

<root>
    <A>
        <B/>
        <C/>
    </A>
    <D>
        <E/>
        <F/>
    </D>
</root>

变为:

<root>
    <A>
        <B/>
        <C/>
    </A>
    <G>
        <E/>
        <F/>
    </G>
</root>

流程图7.jpg

因为DG不是同一类型,所以会将D及子节点都删除,创建G节点及其子节点。

element diff(节点比较)

当节点处于同一层级时,react diff有三种操作方式:

  • INSERT_MARKUP(插入):新的 component 类型不在老集合里, 需要对新节点执行插入操作;

  • MOVE_EXISTING(移动):在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点;

  • REMOVE_NODE(删除):老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。

对同一层级的同组子节点添加唯一 key 进行区分,可以极大的提高了性能。

image.png

上图从旧变成新集合:

  • 首先从新集合中取得 B ,然后判断旧集合中是否存在相同的节点 B,发现存在节点 B(注意 key)

  • B 在旧集合中的位置为 B.moundIndex = 1, 此时 lastIndex = 0,不满足 child._moundIndex < lastIndexB 不进行移动。

  • 更新 lastIndex = Math.max(prevChild._mountIndex,lastIndex),则 lastIndex = 1,并将旧集合中的 B位置更新为新集合中的位置 prevChild._mountIndex,此时新集合中 B.mountIndex = 0 , nextIndex++ 进入下一个节点判断。

  • 从新集合中取得 A,然后判断旧集合中是否存在相同节点 A,存在节点 A

  • A 在旧集合中的位置 A._mountIndex = 0,此时 lastIndex = 1,满足 child._mountIndex < lastIndex 的条件,因此对 A 进行移动操作 enqueueMove(this, child._mountIndex, toIndex),其中 toIndex 其实就是 nextIndex,表示 A 需要移动到的位置。

  • 更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 1,并将 A 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 A._mountIndex = 1nextIndex++ 进入下一个节点的判断。

  • 从新集合中取得 D,然后判断旧集合中是否存在相同节点 D,存在节点 D

  • D 在旧集合中的位置 D._mountIndex = 3,此时 lastIndex = 1,不满足 child._mountIndex < lastIndex 的条件,因此不对 D 进行移动操作。

  • 更新 lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex =3,并将 D 的位置更新为新集合中的位置 prevChild._mountIndex = nextIndex,此时新集合中 D._mountIndex = 2nextIndex++ 进入下一个节点的判断。

  • 从新集合中取得 C,然后判断旧集合中是否存在相同节点 C,存在节点 C

  • C 在旧集合中的位置 C._mountIndex = 2,此时 lastIndex = 3,满足 child._mountIndex < lastIndex 的条件,因此对 C 进行移动操作 enqueueMove(this, child._mountIndex, toIndex)

  • 更新lastIndex = Math.max(prevChild._mountIndex, lastIndex),则 lastIndex = 3,并将 C 的位置更新为新集合中的位置prevChild._mountIndex = nextIndex,此时新集合中 A._mountIndex = 3nextIndex++ 进入下一个节点的判断。

  • C 已经是最后一个节点,diff 操作到此完成。