image.png

React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。

设计理念:快速响应

制约瓶颈:CPU 与 IO 瓶颈

  • 当遇到大计算量的操作或者设备性能不足时页面掉帧,导致卡顿
  • 发送网络请求后,由于需要等待数据返回才能进一步操作导致不能快速响应。

解决方法:实现异步可中断的更新

# 老的 React 架构(React 15)

React15 架构可以分为两层:

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

Diff 算法将上一次更新的组件和本次更新的组件做对比,被判定为需要更新的组件,会被交给渲染器进行渲染,不同的渲染器会将不同的组件渲染到不同的宿主环境的视图中

# Reconcile 协调器

在 React 15 中采用的是 stack reconciler 解决方案

可以通过 this.setState 等 API 来触发更新,每次 update 时,协调器就会开始它的工作

  • 首先会开始 render 阶段的执行,将 JSX 转化成 Fiber Virtual DOM
  • 接着会前后两次的 Virtual DOM 进行对比,也就是 Diff 算法的工作,会对变更的节点打上对应的操作类型 effectTag
  • commit 阶段,会找到本次更新中变化的 Virtual DOM 节点
  • 通知 Renderer 渲染器更新对应的视图

# Renderer 渲染器

React 最初只是服务于 DOM,但是这之后被改编成也能同时支持原生平台的 React Native。因此,在 React 内部机制中引入了“渲染器”这个概念。

渲染器用于管理一棵 React 树,使其根据底层平台进行不同的调用。

  • React DOM Renderer: 将 React 组件渲染成 DOM。它实现了全局 ReactDOMAPI,这在npm上作为 react-dom 包。这也可以作为单独浏览器版本使用,称为 react-dom.js,导出一个 ReactDOM 的全局对象.
  • React Native Renderer: 将 React 组件渲染为 Native 视图。此渲染器在 React Native 内部使用。
  • React Test Renderer: 将 React 组件渲染为 JSON 树。这用于 Jest 的快照测试特性。在 npm 上作为 react-test-renderer 包发布。 另外一个官方支持的渲染器的是 react-art。它曾经是一个独立的 GitHub 仓库,但是现在我们将此加入了主源代码树。
  • ReactArt Renderer (opens new window) :渲染到 Canvas, SVG 或 VML (IE8)

在每次更新发生时,Renderer 接到 Reconciler 通知,将变化的组件渲染在当前宿主环境。

# React15 架构的缺点

Reconciler 阶段, 会递归的更新子组件,调用 mount Componentupdate Component 来实现,这也成为了它致命的缺点,一旦更新无法中断

# 递归更新的缺点

当组件的层级很深时,无法在一帧内完成更新,又没有办法中断本次更新,用户交互就会变得卡顿

在 React 15 架构中,采用同步更新的方式,ReconcilerRenderer 是交替工作的,只有当前一个 DOM 完成了 renderer 才会进入下一个 DOM 的 Reconciler

在用户看来所有的 DOM 是同时更新的。

在前面说到,React 为了践行快速响应的理念,需要实现异步可中断的更新,那么基于 React 15 的架构能够实现吗?

我们来演示一下在 React 15 架构下,如果触发更新的中断会发生什么?

在下面的列子中,左边是更新前的页面,当我们点击时,会触发左侧数字 count + 1。正常情况,我们应当看到页面为 2 3 4

当 2 更新为 3 后,我们中断了更新,由于 React 15 采用的是递归的更新,在上一个 DOM 完成更新之后才会开始下一个 DOM 的更新,因此就会看到右侧的页面,也就是 2 3 3,用户却看不到期望的值,也因此有了 React 16 的 Fiber 架构

image.png

# 新的 React 架构

在 React 16 版本中进行了一次大的重构,React 16 架构解决了 React 15 不能支撑异步更新的问题 React16 架构可以分为三层:

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

可以看到,相较于 React15,React16 中新增了 Scheduler(调度器)模块

在新的架构中,更新首先会被调度器处理,在调度器中会调度这些更新的优先级,更高优的更新会首先进入协调器,在本次更新的 Reconcile 中正在执行 Diff 算法时,如果此时产生了更高优先级的更新,本次正在协调的更新会被中断,由于 SchedulerReconcile 都是在视图中完成的操作,因此即使更新中断,用户也不会看到更新不完整的视图。当某次更新完成了 Reconcile 中的工作时,协调器会通知渲染器,本次更新有哪些组件需要执行对应的视图操作(CRUD),当渲染器完成了它的工作,调度器又会开始新一轮的调度

# Scheduler 调度器

我们知道如果我们的应用占用较长的 js 执行时间,比如超过了设备一帧的时间,那么设备的绘制就会出现卡顿的现象。

Scheduler 主要的功能是时间切片和调度优先级,react 在对比差异的时候会占用一定的 js 执行时间,Scheduler 内部借助 MessageChannel 实现了在浏览器绘制之前指定一个时间片,如果 react 在指定时间内没对比完,Scheduler 就会强制交出执行权给浏览器

在 Scheduler 的实现核心是,判断浏览器是否有剩余时间作为任务中断的标准,在部分浏览器中以及实现了这个 API,requestIdle Callback,但是 React 并没有直接使用这个 API ,而是自行实现了一个功能更加完备的 requestIdleCallback 的 polyfill,也就是 Scheduler。

# Reconcile 协调器

在 React 15 的 reconcile 协调器中,会通过递归的方式来处理虚拟 DOM ,这样导致 Reconcile 过程无法被中断

React 16 推行 Fiber reconciler 的主要目标是:

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

在 React 16 中将更新工作从递归变成了可以中断的循环过程,每次循环都会调用 shouldYield 判断当前是否有剩余时间

function workLoopConcurrent() {
  // 判断是否中断
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}
1
2
3
4
5
6

那么 React 16 是如何解决更新中断时 DOM 渲染不完全的问题呢?

在 React 16 中,Reconciler 与 Renderer 不再是交替工作。当 Scheduler 调度器将任务交给 Reconciler 后,Reconciler 会为变化的 DOM 打上标记 effectTag

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;
1
2
3
4

只有再所有组件都完成了 Reconciler 的工作,才会统一交给 Renderer 渲染器进行更新渲染

# Renderer 渲染器

Renderer 会根据 Reconciler 中为虚拟 DOM 打的 tag,在 commit 阶段同步执行对应的 DOM 操作

同样也有多种不同的渲染器,和 React 15 中保持一致

以上就是 React 的设计理念以及新老架构的演变

# 参考资料

lastUpdate: 6/12/2022, 3:28:00 AM