React diff 策略
讲 react diff 策略的文章很多,很多只说了基础内容,看了 《深入 React 技术栈》 之后,了解更深入了。
一、React diff 策略
React 对于传统的 diff 算法做了一些约定(or 假想或者策略),使得原本 O(n3) 复杂度大部分情况下维持了 O(n) 的复杂度。
React diff 算法的三个策略:
- Web UI 中 DOM 节点跨层级的移动操作非常少,可以忽略不计
- 想同类的两个组件将会组成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
- 对于同一层级的一组子节点,他们可以通过唯一 ID 进行区分
二、 React Tree Diff
对于树的 diff 算法, 基于策略一,这种优化是对树进行分层比较,两棵树只会对同一层次的节点进行比较。
这个策略基于:
针对这一现象,React 通过 updatDepth 对 Virtual DOM树进行层级控制,只会对相同层级的 DOM 节点进行比较。即
当发现节点已经不存在的时候,该节点及其子节点会被完全删除,不会进一步比较。因此只需要对数树进行一次遍历,便能完成整个 DOM 树的比较。
如果出现跨层级的变动:
如果 DOM 节点出现了跨层级的移动,则基于 React 的策略,不会出现 移动 操作,而是会出现 重建-销毁 操作。
因此,如果出现了下图所示的 DOM 操作。 A 节点所在的子树不会直接移动到 D 节点下面,真正的操作是:
或显示节点,而不是真正地移除或添加 DOM 节点。
上图来自 《深入 React 技术栈》 图 3-19
三、React Component Diff
React 对于组件间的 diff 策略如下:
- 如果是同一类型组件,按照元策略继续比较 Virtual DOM 树即可。
- 如果不是同一类型组件,将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
- 对于同一类型组件,有可能其 virtual DOM 没有任何变化,如果能确切的知道这个前提,则可以节省大量的 diff 运算时间。因此 React 允许用户通过
shouldComponentUpdate()
来判断组件是否需要进行 diff 算法分析。
下图所示,如果 D 节点及其子树发生了变化,即使 D 子树和 G 子树结构相似,但是 React 会直接删除 D ,然后重建 G。
(React 认为不同类型的组件,DOM 树相同的情况非常少,这种极端的因素可以忽略不计或者难以造成极大的影响)
上图来自 《深入 React 技术栈》 图 3-20
四、React Element Diff
统一层级的节点进行 diff 的时候,有三种节点操作:
- INSERT_MARKUP (插入):新的组件类型不在旧集合中,即全新的节点,需要对新节点执行插入操作
- MOVE_EXISTING (移动):旧集合中有新组建类型,而且 element 是可更新的类型,
generateComponentChildren
已带哦用receiveComponent
,这种情况下prevChild=nextChild
,就需要做移动操作,可以复用以前的 DOM 节点。 - REMOVE_NODE (删除):旧组件类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合中的,也需要执行删除操作。
这种情况下,diff 算法的工作原理:
- 对集合中的节点进行循环遍历
for (name in nextChildren)
,通过唯一 key 进行判断新旧集合中是否存在相同的节点,if(prevChild === nextChild)
,如果存在相同的节点, 则进行移动操作。 - 但是在移动前需要将当前节点在旧集合中的位置与
lastIndex
进行比较if(child._mountIndex < lastIndex)
,否则不执行操作。
- 这是一种顺序优化方式,
lastIndex
一直在更新,表示访问过的节点在旧集合中最右的位置(最大的位置)。
- 如果新集合中当前当前访问的节点比
lastIndex
大,说明当前访问节点在旧集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不同添加到差异队列中,也就不执行移动操作。 - 只有当访问的节点比
lastIndex
小的时候,才需要继续移动
上图来自 《深入 React 技术栈》 图 3-22
上面节点变化时,首先从新结合中取得 B ,然后判断旧集合中是否存在相同的节点 B,此时发现存在节点 B(注意 key),接着通过对比节点位置判断是否进行移动操作。
B 在旧集合中的位置 B.moundIndex = 1
此时 lastIndex = 0
,不满足 child._moundIndex < lastIndex
,因此不会 B 进行移动。
更新 lastIndex = Math.max(prevChild._mountIndex,lastIndex)
,其中 prevChild._mountIndex
表示 B 在旧集合中的位置,则 lastIndex = 1
,并将 的 位置更新为新集合中的位置 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 操作到此完成。
文章版权:Postbird-There I am , in the world more exciting!
本文链接:http://ptbird.cn/react-diff-from-code.html
转载请注明文章原始出处 !