React的Diff算法

面试

  • 节点(element和component)类型不同:新的替换旧的

  • 节点(element和component)类型相同:element改变替换属性并递归处理该节点的子节点,component一般是使用新的props替换掉旧的props,并在之后调用组件的componentWill/DidReceiveProps方法,之前的组件的render方法会被调用。节点的比较机制开始递归作用于这个它的子节点上。

  • 列表之间的比较:通过key把这个算法的复杂度降低

diff 策略

  1. Web UI 中 DOM 节点跨层级的移动操作特别少。
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分

基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化

tree diff

  • 对树进行分层比较,两棵树只会对同一层次的节点进行比较。

如果出现了 DOM 节点跨层级的移动操作

React diff 的执行情况:create A -> create B -> create C -> delete A。

因此React 官方建议不要进行 DOM 节点跨层级的操作

component diff

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

针对第三种情况

当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。可通过shouldComponentUpdate函数避免component E和component F的更新操作

element diff

React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。

  • INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。

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

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

允许开发者对同一层级的同组子节点,添加唯一 key 进行区分。通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置。

若不使用key更新过程为:老集合中包含节点:A、B、C、D,更新后的新集合中包含节点:B、A、D、C,此时新老集合进行 diff 差异化对比,发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类推,创建并插入 A、D 和 C,删除 B、C 和 D。

使用key更新过程为:新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。

  1. 判断老集合中存在相同节点 B,由于 B 在老集合中的位置 B._mountIndex = 1,此时lastIndex = 0,因此不对 B 进行移动操作;更新 lastIndex = 1,并将 B 的位置更新为新集合中的位置B._mountIndex = 0,nextIndex++进入下一个节点的判断。

  2. 从新集合中取得 E,判断老集合中不存在相同节点 E,则创建新节点 E;更新 lastIndex = 1,并将 E 的位置更新为新集合中的位置E._mountIndex = 1,nextIndex++进入下一个节点的判断。

  3. 判断老集合中存在相同节点 C,由于 C 在老集合中的位置C._mountIndex = 2,lastIndex = 1,此时 C._mountIndex > lastIndex,因此不对 C 进行移动操作;更新 lastIndex = 2,并将 C 的位置更新为新集合中的位置C._mountIndex = 2,nextIndex++ 进入下一个节点的判断。

  4. 判断老集合中存在相同节点 A,由于 A 在老集合中的位置A._mountIndex = 0,lastIndex = 2,此时 A._mountIndex <lastIndex,因此对 A 进行移动操作;更新 lastIndex = 2,并将 A 的位置更新为新集合中的位置,nextIndex++ 进入下一个节点的判断。

  5. 当完成新集合中所有节点 diff 时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点,发现存在这样的节点 D,因此删除节点 D,到此 diff 全部完成。

R is will receive props
R is will updated.
D is will receive props
D is will updated.
B is will receive props
B is will updated.
C is created.
A is created.
C will Unmount.
A will Unmount.
D did updated.
A did mount.
C did mount.
B did updated.
R did updated.

R is will receive props
R is will updated.
D is will receive props
D is will updated.
C is created.
B is will receive props
B is will updated.
C did mount.
D did updated.
B did updated.
R did updated.

// 子组件没有key属性
R is will receive props
R is will updated.
D is created.
C is created.
B is created.
C will Unmount.
B will Unmount.
D will Unmount.
D did mount.
C did mount.
B did mount.
R did updated.

// 带有key属性
R is will receive props
R is will updated.
C is will receive props
C is will updated.
B is will receive props
B is will updated.
D is will receive props
D is will updated.
C did updated.
B did updated.
D did updated.
R did updated.

results for ""

    No results matching ""