React虚拟Dom & Diff算法 & key

694 阅读4分钟

React Virtual Dom

Virtual DOM就是使用JavaScript对象来表示真实DOM,是一个树形结构。

React需要同时维护两棵虚拟DOM树:一棵表示当前的DOM结构;另一棵在状态变更,将要重新渲染时生成(即在下一个state或者props更新的时候,render()函数创建了一个新的虚拟DOM树)。

React使用Diff算法比较两棵树的差异,决定是否需要修改DOM结构以及如何修改。再把所记录的差异应用到真正的DOM树上,视图就更新了。这样保证了每次操作更新后页面的高效渲染,而不是重新渲染整个页面。

Diff 算法

传统diff算法

传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。

Diff 前提策略

  • Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
  • 拥有相同类的两个组件会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  • 对于同一层级的一组子节点,他们可以通过唯一的key进行区分

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

tree diff

由于DOM节点跨层级的移动操作少到可以忽略不计,React 对 Virtual DOM 树进行层级控制,只对同一个父节点下的所有子节点进行比较。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。

preview

如果出现了DOM节点跨层级的移动操作,React diff 会有怎样的表现呢?

如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A。

preview

所以,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,并删除原来的树,这是一种影响 React 性能的操作。

因此,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

component diff

对比每一层的时候,每一层都有自己的组件 ,那么组件之间的对比,叫做component diff。

  • 如果是同一类型的组件,则暂时认为不需要更新。React允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行diff。
  • 如果不是同一类型,替换整个组件下的所有子节点。

element diff

对同一层级的同组子节点添加唯一的key进行区分,优化了插入、移动、删除节点的操作(INSERT_MARKUP, MOVE_EXISTING, REMOVE_NODE)。传统方式下需要挨个节点先删除再增添。

  • 遍历新集合的节点,用lastIndex进行标志
  • 从新集合取得节点B,如果老集合存在相同节点B,则看看节点在老集合中的位置 B._mountIndex。如果B._mountIndex < lastIndex,则对B进行移动操作,否则不移动。
  • 如果老集合中不存在相同节点,则创建新节点。
  • 遍历完新集合所有节点后,最后还要对老集合遍历一次。找到新集合中没有但老集合中还存在的节点,删除。至此diff全部完成。

preview

不足:

ABCD -> DABC :遍历新集合,D不动(D._mounIndex>lastIndex(0)),ABC都要移到D的后面。理想情况下只需要把D放到最前面就行,但Diff算法做不到。

所以要避免将最后一个节点移动到列表首部的操作

总结

  • React diff 策略,将 O(n3) 复杂度的问题转换成O(n) 复杂度的问题

  • 基于 Web UI 中 DOM 节点跨层级的移动操作特别少的前提,React 通过分层求异的策略,对 tree diff 进行算法优化

  • React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化 (组件类型不同,直接替换)

  • React通过设置唯一key的策略,对element diff 进行算法优化

  • 建议在开发组件时保持稳定的DOM结构,有助于性能的提升(不要进行DOM节点跨层级移动)

  • 建议在开发过程中,尽量减少将最后一个节点移动到列表首部的操作

参考: