React技术揭秘1-2 React架构

1,849 阅读6分钟

本文节选自 React技术揭秘

React从v15升级到v16后重构了整个架构。本文从v15的架构聊起,讲讲为什么会重构,以及最新版的React16架构是什么样的。

React15架构

React15架构可以分为两层:

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

Reconciler(协调器)

我们知道,在React中可以通过this.setStatethis.forceUpdateReactDOM.render等API触发更新。

每当有更新发生时,Reconciler会做如下工作:

  • 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
  • 将虚拟DOM和上次更新时的虚拟DOM对比
  • 通过对比找出本次更新中变化的虚拟DOM
  • 通知Renderer将变化的虚拟DOM渲染到页面上

你可以在这里看到React官方对Reconciler的解释

Renderer(渲染器)

由于React支持跨平台,所以不同平台有不同的Renderer。我们前端最熟悉的是负责在浏览器环境渲染的Renderer —— ReactDOM

除此之外,还有:

  • ReactNative渲染器,渲染App原生组件
  • ReactTest渲染器,渲染出纯Js对象用于测试
  • ReactArt渲染器,渲染到Canvas, SVG 或 VML (IE8)

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

你可以在这里看到React官方对Renderer的解释

React15架构的缺点

Reconciler中,mount的组件会调用mountComponentupdate的组件会调用updateComponent。这两个方法都会递归更新子组件。

递归更新的缺点

主流的浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次。我们知道,JS是可以操作DOM的,所以JS脚本执行浏览器布局、绘制是处于同一线程。

在每16.6ms时间内,该线程需要完成如下工作:

JS脚本执行 -----  样式布局 ----- 样式绘制

当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局样式绘制了。

对于用户在输入框输入内容这个行为来说,就体现为按下了键盘按键但是页面上不实时显示输入。

对于React的更新来说,由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。

在上一节中,我们已经提出了解决办法——用可中断的异步更新代替同步的更新。那么React15的架构支持异步更新么?让我们看一个例子:

乘法小Demo

初始化时state.count = 1,每次点击按钮state.count++。列表中3个元素的值分别为1,2,3乘以state.count的结果 :::

我用红色标注了更新的步骤。 更新流程

我们可以看到,ReconcilerRenderer是交替工作的,当第一个li在页面上已经变化后,第二个li再进入Reconciler

由于整个过程都是同步的,所以在用户看来所有DOM是同时更新的。

让我们看看在React15架构中如果中途中断更新会怎么样? 中断更新流程

当第一个li完成更新时中断更新,即步骤3完成后中断更新,此时后面的步骤都还未执行。用户会在页面上看见更新不完全的DOM!

基于这个原因,React决定重写整个架构。接下来让我们看看React16的架构是什么样的。

React16架构

React16架构可以分为三层:

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

可以看到,相较于React15,React16中新增了Scheduler(调度器),让我们来了解下他。

Scheduler(调度器)

既然我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。

其实部分浏览器已经实现了这个API,这就是requestIdleCallback。但是由于以下因素,React放弃使用:

  • 浏览器兼容性
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低

基于以上原因,React实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

Schduler是独立于React的库

Reconciler(协调器)

我们知道,在React15中Reconciler是递归处理虚拟DOM的。让我们看看React16的Reconciler

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

/** @noinline */
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

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

在React16中,ReconcilerRenderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,类似这样:

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

全部的标记见这里

整个SchedulerReconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer

你可以在这里看到React官方对React16新Reconciler的解释

Renderer(渲染器)

Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。

所以,对于我们在上一节使用过的Demo

乘法小Demo

在React16架构中整个更新流程为: 更新流程

其中红框中的步骤随时可能由于以下原因被中断:

  • 有其他更高优任务需要先更新
  • 当前帧没有剩余时间

由于红框中的工作都在内存中进行,不会更新页面上的DOM,即使反复中断用户也不会看见更新不完全的DOM。

实时上,由于SchedulerReconciler都是平台无关的,所以React为他们单独发了一个包react-Reconciler。你可以用这个包自己实现一个ReactDOM,具体见参考资料

参考资料

React前经理Sophie在React Conf 2019的演讲

「英文 外网 计划翻译」Building a Custom React Renderer | Sophie Alpert