对diff算法的理解

390 阅读2分钟

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

我们知道在react中,DOM节点跨层级的移动操作比较少,可以忽略不计。有相同类的两个组件会生成类似的树形结构。还有对同一层级的一组子节点,可以通过唯一id进行区分。针对这三个特点,react分别对树、组件、元素进行了算法优化,将时间复杂度降低到了O(n)。

tree diff

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

当出现节点跨层级移动的时候,只存在创建和删除操作,节点和所有的子节点都会重新创建,非常耗费性能,因此我们要尽量保持稳定的DOM结构,,比如可以通过css隐藏节点,而不是真的删除节点。

component diff

对组件来说,同一类型对组件就继续按照原策略比较vdom tree, 如果不是就将这个组件判断为dirty component,替换这个组件下所有子节点。

对于同一类型对组件,有可能vdom没有发生任何变化,因此react允许用户通过shouldComponentUpdate来判断组件是否需要进行diff。

element diff

对同级元素来说,由于很多情况都是相同的节点,如果仅因为位置变化就删除重建,性能太低。于是react允许对同一层级的同组子节点进行唯一key值区分。

首先对新集合的节点进行遍历,通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。lastIndex 表示老集合中最右的位置,如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。

diff算法的不足

diff算法也存在一些不足,新集合的节点更新为:D、A、B、C,与老集合对比只有 D 节点移动,而 A、B、C 仍然保持原有的顺序,理论上 diff 应该只需对 D 执行移动操作,然而由于 D 在老集合的位置是最大的,导致其他节点的 _mountIndex < lastIndex,造成 D 没有执行移动操作,而是 A、B、C 全部移动到 D 节点后面的现象。

因此在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。