开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
本篇讲解diff算法基于React16之前
传统diff算法
通过循环递归进行对比,算法的时间复杂度达 。
React的diff算法
- 什么是调和?
将virtual DOM(虚拟dom)转换成actual DOM(真实dom)的 最少操作过程就叫调和,简单理解就是简化算法复杂度。 - react的diff算法
react的diff算法就是通过深度优先算法实现了上述的调和,简化了算法的复杂度。 - diff算法实现基础
- 只比较同一层级的节点
- 如果两个节点类型不一样,以这两个节点为根节点的树会完全不同
- 开发者会用
key标识出来多次render中结构保持不变的节点,以便重用
Diff策略
React通过tree diff、component diff、element diff三大策略将算法的时间复杂度复杂度从降为 。
tree diff(树比较)
- React通过updateDepth对virtual DOM树进行层级控制
- 对树分层进行比较,两棵树只对同一层的节点进行比较,如果节点不存在,则删除这个节点以及此节点下所有子节点,不会再进一步比较
- 只需遍历一次,就能完成整棵DOM树的比较
如果遇到下面这样的情况:
以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>
因为D和G不是同一类型,所以会将D及子节点都删除,创建G节点及其子节点。
element diff(节点比较)
当节点处于同一层级时,react diff有三种操作方式:
-
INSERT_MARKUP(插入):新的 component 类型不在老集合里, 需要对新节点执行插入操作;
-
MOVE_EXISTING(移动):在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点;
-
REMOVE_NODE(删除):老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
对同一层级的同组子节点添加唯一 key 进行区分,可以极大的提高了性能。
上图从旧变成新集合:
-
首先从新集合中取得
B,然后判断旧集合中是否存在相同的节点B,发现存在节点B(注意 key)。 -
B在旧集合中的位置为B.moundIndex = 1, 此时lastIndex = 0,不满足child._moundIndex < lastIndex,B不进行移动。 -
更新
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 = 1,nextIndex++进入下一个节点的判断。 -
从新集合中取得
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 = 2,nextIndex++进入下一个节点的判断。 -
从新集合中取得
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 = 3,nextIndex++进入下一个节点的判断。 -
C已经是最后一个节点,diff 操作到此完成。