React 栈调和过程是怎样的

从 React 15 入手,理解 栈调和 算法

调和过程与 Diff 算法

调和(Reconciliation),又译为 协调。

通过如 ReactDOM 等类库使之与 真实 DOM 同步。 这一过程叫做协调(调和)。

调和指的是将虚拟 DOM 映射到真实 DOM 的过程,Diff 过程只是其中一个环节。

React 源码结构佐证了这一点:React 从大的板块上将源码划分为 Core、Renderer、Reconciler 三部分。

其中 Reconciler 调和器所做的工作是一系列的,包括组件的挂载、卸载、更新等过程,其中更新过程涉及对 Diff 算法的调用。

所以,调和 !== Diff。但 Diff 确实是调和过程中最具有代表性的一环。

根据 Diff 实现形式的不同,调和过程被划分为 以 React 15 为主的"栈调和" 以及 以 React 16 为主的"Fiber 调和"。

Diff 策略的设计思想

Diff 算法本质上是找不同的过程。

计算机领域上找出两个树结构之间的不同,需要通过循环递归进行树节点的一一对比。这个时间复杂度是 O(n^3)。

但 O(n^3) 的复杂度是不能被接受的,所以我们需要优化。

React 团队总结了以下两个规律,为将 O(n^3) 转换成 O(n) 确定了大前提:

  • 若两个组件属于同一个类型,那么它们将拥有相同的 DOM 树形结构。
  • 处于同一层的一组子节点,可通过设置 key 作为唯一标识,从而维护各个节点在不同渲染过程中的稳定性。

除了这两个规律之外,还有一个规律,为 React 实现高效的 Diff 提供了灵感:DOM 节点之间的跨层级操作并不多,同层级操作是主流

把握三个要点,解释 Diff 逻辑

  1. Diff 算法性能突破的关键点在于"分层对比"
  2. 类型一致的节点才有继续 Diff 的必要性
  3. key 属性的设置,可以帮我们尽可能重用同一层级内的节点

分层对比

结合第三规律:同层级操作是主流。React 的 Diff 过程 直接放弃了跨层级的节点比较,它只针对相同层级的节点做对比。

如此一来,只需要从上到下做一次遍历,就可以完成整棵树的对比。

销毁 + 重建的代价是昂贵的,因此 不要做跨层级的操作,尽量保持 DOM 结构的稳定性。

减少递归:类型的一致性决定递归的必要性

本着主要矛盾的原则,React 任务,只有同类型的组件,才有进一步对比的必要性。

若 参与 Diff 的两个组件类型不同,那么直接放弃比较,原地替换掉旧节点。只有相同类型的组件,React 才会保留组件对应 DOM 树 或 子树,进行更深层次的遍历。

重用节点:key 属性

key 是用来帮助 React 识别哪些内容被更改、添加或删除。key 需要卸载用数组渲染出来的元素内部,并且需要赋予一个稳定的值。如果 key 发生了变更,React 则会直接触发重渲染。

解决的问题:同一层级下节点的重用问题。