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树的比较。
如果出现了DOM节点跨层级的移动操作,React diff 会有怎样的表现呢?
如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A。
所以,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 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全部完成。
不足:
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节点跨层级移动)
-
建议在开发过程中,尽量减少将最后一个节点移动到列表首部的操作。