React 源码学习之 React 设计思想

794 阅读7分钟

luke-chesser-LG8ToawE8WQ-unsplash.jpg

图片来源 unsplash.com/photos/LG8T…

React 设计理念

从 React 官网 我们可以看到这样一句话:

React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。

我们知道目前主流浏览器刷新频率为 60Hz,即每(1000ms / 60Hz)16.6ms 浏览器刷新一次。在这 16.6ms 要完成以下事情:

16.6ms.png

  1. 首先 JS 脚本执行;
  2. 执行请求动画帧 requestAnimationFrame(rAF),即在每次绘制之前,会执行 rAF 回调;
  3. 进行 Layout 操作,包括计算布局和更新布局;
  4. 进行 Paint 操作,得到树中每个节点的尺寸与位置等信息,浏览器针对每个元素进行内容填充;
  5. 接下来处于空闲阶段(Idle Peroid),可以在这时执行 requestIdleCallback 里注册的任务; 如果我们在 16.6ms 内没有完成,页面会出现一定程度的卡顿现象。

React v15 架构

React v15 (1).png

React 15 架构分为两层:

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

Reconciler(协调器)

React 中可以通过 this.setState、this.forceUpdate、ReactDOM.render 等API触发更新。

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

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

Renderer(渲染器)

React 支持跨平台,不同平台有不同的 Renderer。浏览器的Renderer 是 ReactDOM 库。 除此之外,还有其他平台的 Renderer:

  1. ReactNative:渲染 App 原生组件;
  2. ReactTest:渲染出纯 JS 对象用于测试;
  3. ReactArt:渲染到 Canvas,SVG 或 VML(IE8);

React 15 架构的缺点

在 React 15 版本中 Reconciler 叫 “stack” reconciler ,它会 mount(组件挂载) 时调用 mountComponent,update(组件更新) 的组件会调用 updateComponent。这两个方法都会使用递归更新子组件,无法实现异步可中断的更新,调度优先级。如果组件树的层级很深,递归会占用线程很多时间,导致的 JS 执行时间过长,超过16.6ms(主流的浏览器刷新频率为60Hz,即每(1000ms / 60Hz)),容易造成页面卡顿。

React 16 架构

React 16 (1).png

React 16 架构可以分为三层:

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

Scheduler(调度器)

我们知道部分浏览器已经实现了 requestIdleCallback ,当浏览器有剩余时间时通知我们。但是由于以下因素,React放弃使用:

  1. 浏览器兼容性;
  2. 触发频率不稳定;

基于以上原因,React 团队自己实现了 requestIdleCallback 的 polyfill,就是 Scheduler。除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置

Reconciler (协调器)

文件位于:github.com/facebook/re…

React15React16,协调器(Reconciler)重构的一大目的是:将老的同步更新的架构变为异步可中断更新。更新在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态。适时地让出 CPU 执行权,可以让浏览器及时地响应用户的交互。

Generator

React 团队为什么没有使用 Generator 来实现异步可中断呢?React 团队成员 sebmarkbage 在 16 年的 issue Fiber Principles: Contributing To Fiber 做出了解答。 放弃的主要原因如下:

  1. Generator 具有传染性,使用了 Generator 则上下文的其他函数也需要作出改变。
  2. 生成器是有状态的,无法在其中途恢复。

Fiber reconciler

“fiber” reconciler 是一个新尝试,致力于解决 stack reconciler 中固有的问题,同时解决一些历史遗留问题。Fiber 从 React 16 开始变成了默认的 reconciler。

Fiber 包含三层含义:

  1. 作为架构来说,之前 React15Reconciler 采用递归的方式执行,数据保存在递归调用栈中,所以被称为 stack Reconciler。React16 的 Reconciler 基于 Fiber 节点实现,被称为 Fiber Reconciler
  2. 作为静态的数据结构来说,每个 Fiber 节点对应一个 React element,采用链表实现。保存了该组件的类型、对应的DOM节点等信息。
  3. 作为动态的工作单元来说,每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。

Fiber 主要目标是:

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

Fiber 结构

Fiber 结构在 React 源码中的定义,如下所示。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他 Fiber 节点形成 Fiber 树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;
  
  // 作为动态的工作单元的属性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;
  
  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  
  // 指向该fiber在另一次更新时对应的fiber
  this.alternate = null;
}
作为架构
// 指向父级 Fiber 节点
this.return = null;
// 指向子 Fiber 节点
this.child = null;
// 指向右边第一个兄弟 Fiber 节点
this.sibling = null;

比如像下面的例子:

function App() {
  return (
    <div>
      <span> Hello </span>
      <span> World </span>
    </div>
  )
}

对应的 Fiber 树结构:

未命名绘图.png

作为静态的数据结构
// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指 DOM 节点 tagName
this.type = null;
// Fiber对应的真实 DOM 节点
this.stateNode = null;
作为动态的工作单元

作为动态的工作单元,Fiber 中如下参数保存了本次更新相关的信息。

// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

// 保存本次更新会造成的 DOM 操作
this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;

Fiber 双缓存树

在 React 中最多会同时存在两棵 Fiber 树。当前屏幕上显示内容对应的 Fiber 树称为current Fiber 树,正在内存中构建的 Fiber 树称为 workInProgress Fiber 树。

current Fiber 树中的 Fiber 节点被称为 current fiber,workInProgress Fiber树中的 Fiber 节点被称为 workInProgress fiber,他们通过 alternate 属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React 应用的根节点通过使 current 指针在不同 Fiber 树的 rootFiber 间切换来完成current Fiber 树指向的切换。

workInProgress Fiber 树构建完成交给 Renderer 渲染在页面上后,应用根节点的current 指针指向 workInProgress Fiber 树,此时 workInProgress Fiber 树就变为current Fiber 树。

每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成DOM更新。看下面的例子:

1. 组件 mount
import React from "react";
import ReactDOM from "react-dom";

function App() {
  const [num, addNum] = React.useState(0);
  return <div onClick={() => addNum(num + 1)}>{num}</div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
  • 首次执行 ReactDOM.render 会创建 fiberRootNoderootFiber。其中fiberRootNode 是整个应用的根节点,rootFiber 是所在组件树的根节点。之所以要区分 fiberRootNode 与 rootFiber,是因为在应用中我们可以多次调用 ReactDOM.render 渲染不同的组件树,他们会拥有不同的 rootFiber 。但是整个应用的根节点只有一个,那就是 fiberRootNode。fiberRootNode 的 current 指针会指向当前页面上已渲染内容对应 Fiber 树,即 current Fiber 树。

未命名绘图 (1).png

由于是首屏渲染,页面中还没有挂载任何 DOM ,所以 fiberRootNode.current 指向的rootFiber 没有任何子 Fiber 节点,此时 current Fiber 树为空

  • 接下来进入 render 阶段,根据组件返回的 JSX 在内存中依次创建 Fiber 节点并连接在一起构建 Fiber 树,被称为 workInProgress Fiber 树。在构建 workInProgress Fiber 树时会尝试复用 current Fiber 树中已有的 Fiber 节点内的属性,在首屏渲染时只有 rootFiber 存在对应的 current fiber。

未命名绘图 (5).png

  • 已构建完的 workInProgress Fiber 树在 commit 阶段渲染到页面。

未命名绘图 (6).png

2. 组件 update
  • 当我们点击 div 节点触发状态改变,开启一次新的 render 阶段并构建一棵新的workInProgress Fiber 树。

屏幕录制2021-04-21 上午6.gif

未命名绘图 (8).png

  • workInProgress Fiber 树在 render 阶段完成构建后进入 commit 阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为 current Fiber 树。

未命名绘图 (11).png

参考资料