React架构
上一篇 我们介绍了 React 的设计理念,可以简单概括为快速响应。
快速响应的关键是解决 CPU 和 IO 的瓶颈,实现上需要将同步的更新变为可中断的异步更新。
这对应了 React 架构的演变过程(v15 → v16)。
React 15
React15架构可以分为两层:
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上

Stack Reconciler
Reconciler 负责找出变化的组件,这个“找变化”的过程在 React 中叫做 协调。
我们知道在 React 15 中,可以通过以下的方式触发组件更新,
this.setState
this.forceUpdate
ReactDOM.render
每当有更新发生时,协调器 Reconciler 都会做如下的工作,
- 调用函数组件或 Class 组件的 render 方法,将返回的 JSX 转化为 Virtual DOM
- 对比 Virtual DOM 和上次更新时的 Virtual DOM ,找出本次更新中变化的 Virtual DOM
- 通知 Renderer 将变化的 Virtual DOM 渲染到页面上
虚拟 DOM 的比较本质上是对两个树进行对比,即使使用 最优的算法,其时间复杂度仍为 O(n3)
,其中 n
是树中元素的数量。
如果在 React 中使用该算法,那么展示 1000 个元素则需要 10 亿次的比较,这个开销实在是太过高昂,于是 React 在以下两个假设的基础之上提出了一套 O(n)
的启发式算法:
- 两个不同类型的元素会产生出不同的树
- 开发者可以使用 key 属性标识哪些子元素在不同的渲染中可能是不变的
基于这两个假设实现的启发式算法叫做 Diffing 算法,为了更好地理解协调器,我们简单了解下 Diffing 的过程。
Diffing 算法
Diffing 算法从粒度上可以分为三个层次:类型不同的元素,类型相同的元素以及子元素。
1. 对比类型不同的元素
Diffing 是一个递归的过程,假设 1 是复杂度从 O(n3)
降到 O(n)
的关键策略。当对比两棵树时,React 首先会比较两棵树的根节点(逐层对比),这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较,

当节点为不同类型的元素时,该节点及其子节点都会被完全删除并重建,以下面的 DOM 结构为例,
<!-- 旧的DOM -->
<div>
<Counter />
</div>
<!-- 新的DOM -->
<span>
<Counter />
</span>
React 会销毁根节点 div
及其子节点 Counter
,并重建一个新的根节点 span
以及新的子节点 Counter
组件。
2. 对比类型相同的元素
当节点类型相同时,分两种情况,
- 节点是两个类型相同的 React 元素,React 会保留 DOM 节点,仅比对及更新有改变的属性
- 节点是两个类型相同的组件,React 会保留组件实例,更新该实例的 props,并调用实例的生命周期方法
比如下面的两个节点,
<div className="before" title="stuff" style={{color: 'red', fontWeight: 'bold'}} />
<div className="after" title="stuff" style={{color: 'green', fontWeight: 'bold'}} />
React 会修改 DOM 元素上的 className
和 style
属性,其中 style
属性 React 仅更新有改变的 color
属性值。
3. 对子节点进行递归
当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表,产生差异时,生成一个 mutation
,
针对可能的差异,React 定义了新增、删除和替换等 mutation 标记,
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
不难发现子元素列表末尾新增元素时,更新开销比较小,但如果只是简单的将新增元素插入到表头,那么更新开销会比较大,
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
因为 React 无法判断应该保留 <li>Duke</li>
和 <li>Villanova</li>
,而是会重建每一个子元素,这种情况会带来性能问题。
为了解决上述问题,React 引入了 key
属性。当子元素拥有 key
时,React 使用 key
来匹配以提高树的转换效率。
key 不需要全局唯一,但在子元素列表中需要保持唯一。
Diffing 的结果会交给渲染器 Renderer,渲染器会将差异变化更新到页面。
Renderer
每次更新发生时,Renderer 会接到 Reconciler 通知,将变化的组件渲染在当前宿主环境,
我们知道 React 支持跨平台,所以不同平台有不同的 Renderer,常见的有,
- ReactDOM,渲染 DOM 组件
- ReactNative,渲染App原生组件
- ReactTest,渲染出纯Js对象用于测试
- ReactArt,渲染到Canvas, SVG 或 VML (IE8)
同步更新的问题
React 15 架构是同步更新,在 Reconciler 中,mount 的组件会调用mountComponent,update 的组件会调用updateComponent,这两个方法都会递归更新子组件。
递归更新带来的问题就是,更新一旦开始,就无法中断,当层级很深时,递归更新时间超过了一帧,用户交互就会卡顿,

为了解决这个问题,React 16 进行了架构重构,用可中断的异步更新代替同步的更新。
React 16
React16架构可以分为三层:
- Scheduler(调度器)—— 调度任务的优先级,高优先级的任务会优先进入
Reconciler
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上

相比于 v15,引入了调度器 Scheduler,同时将 Stack Reconciler 重构为 Fiber Reconciler。
Scheduler
前面我们介绍过,可中断的异步更新的实现,需要以浏览器是否有剩余时间作为任务中断的标准,
所以我们需要一种机制,在浏览器有剩余时间时通知我们,怎么判断浏览器是否有剩余时间呢?
可以通过原生的 requestIdleCallback,但由于以下因素,React 放弃使用 requestIdleCallback,
- 浏览器兼容性
- 触发频率不稳定,受很多因素影响。比如切换 Tab 后,之前注册的 requestIdleCallback 触发频率会变低
为此,React 实现了功能更完备的 requestIdleCallback polyfill
,这就是 Scheduler。
除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置。
Fiber Reconciler
为了实现可中断的异步更新,Reconciler 的更新工作从递归变成了可以中断的循环过程。
每次循环都会调用 shouldYield
判断当前是否有剩余时间,
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
同时,Reconciler 与 Renderer 不再是交替工作,整个 Scheduler 与 Reconciler 的工作都在内存中进行。只有当所有组件都完成 Reconciler 的工作,才会统一交给 Renderer,这样就解决了中断更新时 DOM 渲染不完全的问题,

这个新的协调器叫做 Fiber Reconciler。
重构后的 Fiber Reconciler 具有以下特性,
- 能够把可中断的任务切片处理
- 能够调整优先级,重置并复用任务
- 能够在父元素与子元素之间交错处理,以支持 React 中的布局
- 能够在
render()
中返回多个元素。 - 更好地支持错误边界
当然 Fiber
不止作为架构,同时也是静态的数据结构及动态的工作单元,接下来我们将花较大的篇幅深入理解 Fiber
。
参考链接
写在最后
本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!
如果有疑问或者发现错误,可以在评论区进行提问和勘误,
如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
转载自:https://juejin.cn/post/7194391266690826295