【设计理念】

架构的演进

让我们从一个很简单的功能着手:

tsx

点击上面的按钮,视图上显示的值会加 1。

让我们看看 React15 是如何思考上面的功能的:

  1. 通过一段描述(即 JSX)来告诉 React 你想要渲染怎样的 DOM 节点
  2. 让 JSX 翻译成 虚拟 DOM,并交给 Renderer(渲染器)将虚拟 DOM 渲染成节点(mount 阶段)
  3. 点击按钮后,通知 Reconciler(协调器)视图需要更新
  4. Reconciler 内部产生新的虚拟 DOM,并将新旧虚拟 DOM 对比(Diff 算法)并提取出发生变化的部分。
  5. 将产生差异的虚拟 DOM 交给 Renderer,Renderer 更新浏览器视图上的 DOM 节点

一张图表示 React15 的架构:

image-20230119170342273

这个架构下,当数据不断更新时(假设用户不断点击按钮),会按照顺序做以下事情:

  1. Reconciler 用 Diff 算法计算出需要更新的范围,并将结果提交给 Renderer
  2. Renderer 根据差异结果更新真实的 DOM 节点
  3. 浏览器视图发生变化
  4. ... 上面的过程不断循环

整个处理过程像单一的队列般有序,最明显的是,整个过程是同步处理的。

这也意味着 React 的架构遇到了类似于 HTTP 的队首阻塞问题——一旦有一个地方产生了阻塞(用户的 input 行为造成大面积的视图更新时),浏览器忙于处理这些数据层面的运算导致页面卡顿。

而这个问题,使用debounce和throttle都不能很好地解决。

最佳的解决方式是异步,异步能通过优秀的调度来避免阻塞——浏览器已经证明了这一点。

在浏览器内部的异步 Schedule 中,采用双队列来循环调度 JS 代码的执行并协调视图的更新。

为了达成异步更新以提高 React 的性能,React 重写源码升级了新的架构,新架构下 React 实现了一个异步可中断的 Scheduler(调度器)。

image-20230119175452520

新架构下的更新规则变成了:

  1. Scheduler 判断更新等级,是否有更高的优先级需要被调度
  2. 如果没有则将更新交给 Reconciler
  3. Reconciler 接受到更新并创建新的虚拟 DOM 树与旧的虚拟 DOM 做对比
  4. 对比完成后给需要更新的 DOM 打上 Update 标记交给 Renderer
  5. Renderer 更新视图上的真实 DOM

在新架构中,Scheduler 负责接受更新任务,并对更新任务的优先级进行排序。

当把 A 更新推送给 Reconciler 时,Reconciler 会进行 Diff 运算。在这过程中如果 Scheduler 又接收到一个更高级的 B 更新,则中断处理 A 更新的计算,转而再去计算 B 更新。

Fiber 架构

Fiber 是 React16 重写代码后产生的一种架构,它有三种含义:

  1. 新版本的 Reconciler 也可以称为 Fiber Reconciler

    image-20230121153259642

  2. Fiber 作为静态数据结构,它保存各个节点的虚拟 DOM

    以下面代码为例:

    js
    1import React, { useState } from 'react'
    2
    3export default function App() {
    4  const [state, setState] = useState(0)
    5  return (
    6    <div>
    7      <button onClick={() => setState((n) => n + 1)}>click me</button>
    8      <div>{state}</div>
    9    </div>
    10  )
    11}
    12ReactDOM.render(<App />, document.getElementById('root'))

    在 React 内部生产的 Fiber 节点的数据结构是这样的

    image-20230121155857189

    • 首先会生成一个根 FiberRootNode
    • ReactDOM.render 会生成RootFiber 节点,一个应用中可能存在多个 RootFiber
    • 从 RootFiber 开始,每个父节点的 child 属性保存着子节点的信息
    • 到 RootFiber 为止,每个子节点的 return 属性保存着父节点的信息
    • 从FiberRootNode往下是递的过程,从最后的子节点往上则是归的过程
  3. 作为动态的工作单元保存着 Hooks 的副作用和状态等信息

Fiber 工作原理

Fiber 架构采用双缓存来保存生成的虚拟 DOM,以下图为例

image-20230121200908397

当ReactDOM.render首次挂载组件时,React 会创建一个RootFiber节点,并让 FiberRootNode 的 current 属性指向 RootFiber。

当首屏渲染时,React 会创建右侧的 RootFiber,并且递归返回第一批虚拟 DOM 节点。该虚拟 DOM 节点的 RootFiber 与第一个 RootFiber 节点之间用 alternate 属性连接。

当第二个 RootFiber 正在构建时,这个虚拟 DOM 树称为WorkInProgress Fiber 树。

FiberRootNode 的 current 指向的 Fiber 树称为current Fiber树。

image-20230121201827130

当 workInProgress树完成后,FiberRootNode会将 current 指向它的 RootFiber节点,此时它变成了current Fiber 树。

image-20230121202036511

接下来是点击 P 标签触发更新的逻辑。当用户点击 P 标签时,会基于 current Fiber 树的 RootFIber 的 alternate 指向的那个 Fiber 树创建一棵新的 workInProgress Fiber 树。

image-20230121202548921

JSX 调用后返回的数据结构与 current Fiber 树做对比并构建出 workInProgress Fiber 树的算法叫做 Diff 算法。

当 workInProgress 树构建完成后,FiberRootNode 的 current 又指向它,它又会变成 current Fiber 树。

image-20230121203941464

每个 Fiber 节点之间都会用 alternate 属性做连接。