likes
comments
collection
share

React架构

作者站长头像
站长
· 阅读数 14

上一篇 我们介绍了 React 的设计理念,可以简单概括为快速响应

快速响应的关键是解决 CPU 和 IO 的瓶颈,实现上需要将同步的更新变为可中断的异步更新

这对应了 React 架构的演变过程(v15 → v16)。

React 15

React15架构可以分为两层:

  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上
React架构

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) 的启发式算法:

  1. 两个不同类型的元素会产生出不同的树
  2. 开发者可以使用 key 属性标识哪些子元素在不同的渲染中可能是不变的

基于这两个假设实现的启发式算法叫做 Diffing 算法,为了更好地理解协调器,我们简单了解下 Diffing 的过程。

Diffing 算法

Diffing 算法从粒度上可以分为三个层次:类型不同的元素,类型相同的元素以及子元素。

1. 对比类型不同的元素

Diffing 是一个递归的过程,假设 1 是复杂度从 O(n3) 降到 O(n) 的关键策略。当对比两棵树时,React 首先会比较两棵树的根节点(逐层对比),这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较,

React架构

当节点为不同类型的元素时,该节点及其子节点都会被完全删除并重建,以下面的 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 元素上的 classNamestyle 属性,其中 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,常见的有,

同步更新的问题

React 15 架构是同步更新,在 Reconciler 中,mount 的组件会调用mountComponent,update 的组件会调用updateComponent,这两个方法都会递归更新子组件

递归更新带来的问题就是,更新一旦开始,就无法中断,当层级很深时,递归更新时间超过了一帧,用户交互就会卡顿,

React架构

为了解决这个问题,React 16 进行了架构重构,用可中断的异步更新代替同步的更新

React 16

React16架构可以分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优先级的任务会优先进入 Reconciler
  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上
React架构

相比于 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 渲染不完全的问题,

React架构

这个新的协调器叫做 Fiber Reconciler

重构后的 Fiber Reconciler 具有以下特性,

  • 能够把可中断的任务切片处理
  • 能够调整优先级,重置并复用任务
  • 能够在父元素与子元素之间交错处理,以支持 React 中的布局
  • 能够在 render() 中返回多个元素。
  • 更好地支持错误边界

当然 Fiber 不止作为架构,同时也是静态的数据结构及动态的工作单元,接下来我们将花较大的篇幅深入理解 Fiber

参考链接

写在最后

本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!

如果有疑问或者发现错误,可以在评论区进行提问和勘误,

如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。