理念

项目庞大、组件数量繁多时,容易遇到 CPU 的瓶颈。主流浏览器刷新频率为 60Hz,即每 16.6ms(1000ms/60)浏览器刷新一次。GUI 渲染线程与 JS 线程是互斥的,JS 脚本执行和浏览器布局、绘制不能同时执行,这意味着每 16.6ms 需要完成 JS 脚本执行、样式布局、样式绘制这些工作,若 JS 执行时间超出了 16.6ms,该次刷新就没有时间执行样式布局和样式绘制,造成页面掉帧、卡顿

React 提出的解决方案是在浏览器每一帧的时间中,预留一些时间给 JS 线程,React 利用这部分时间更新组件,当预留的时间不够用时,React 将线程控制权交还给浏览器使其有时间渲染 UI,React 则等待下一帧时间到来继续被中断的工作。长任务被拆分到每一帧,这里的关键是将同步的更新变为可中断的异步更新

// https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L119
// Scheduler periodically yields in case there is other work on the main
// thread, like user events. By default, it *yields multiple times per frame*.
// It does not attempt to align with frame boundaries, since most tasks don't
// need to be frame aligned; for those that do, use requestAnimationFrame.
let yieldInterval = 5; // 5ms

https://reactjs.org/blog/2022/03/29/react-v18.html

网络延迟作为 IO 瓶颈的一种,客观存在,频繁地展示 loading 状态对用户的体验不友好,改进的方法是在当前页面停留一小段时间并利用这一小段时间来请求数据,然后再进行页面跳转,这样可以避免一闪而过的 loading 状态。React 的 Suspense 机制实现了这一效果,这一特性同样需要将同步的更新变为可中断的异步更新。

异步可中断更新可以理解为更新在执行过程中可能会被打断(浏览器时间分片用尽或有更高优任务插队),当可以继续执行时恢复之前执行的中间状态。

React 的理念是快速响应,它对 CPU 瓶颈和 IO 瓶颈的处理方式反映出了这一思想。

React15 架构分为两层:

  • Reconciler(协调器) —— 负责找出变化的组件;
    • 更新发生时,协调器调用函数组件或 class 组件的 render 方法,将返回的 JSX 转化为虚拟 DOM,与上次更新时的虚拟 DOM 进行对比,找出本次更新中变化的虚拟 DOM,通知渲染器将变化的虚拟 DOM 渲染到页面上。
    • mount 的组件会调用 mountComponent,update 的组件会调用 updateComponent,这两个方法都会递归更新子组件(即创建虚拟 DOM,数据保存在递归调用栈中)。由于是递归执行,更新一旦开始,中途就无法中断,当层级很深,递归更新时间超过了 16ms,用户交互就会卡顿。显然,由于无法中断,也就无法支持异步更新,出于这一原因,React 16 重构了架构。
  • Renderer(渲染器) —— 负责将变化的组件渲染到页面上。
    • 更新发生时,渲染器接到协调器的通知,将变化的组件渲染在当前宿主环境;
    • React 支持跨平台,所以不同平台有不同的渲染器(ReactDOM,ReactNative,ReactTest,ReactArt)。

React16 架构分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优先级的任务优先进入协调器;
    • 是否中断任务的判据是浏览器是否有剩余时间,调度器会在浏览器有剩余时间时发出通知;
    • Scheduler 是独立的库。
  • Reconciler(协调器) —— 负责找出变化的组件;
    • 更新工作从递归变成了可以中断的循环过程(Fiber 架构);

      function workLoopConcurrent() {
          // Perform work until Scheduler asks us to yield
          while (workInProgress !== null && !shouldYield()) {
      		// 创建下一个 Fiber 节点并赋值给 workInProgress,并将 workInProgress 与已创建的 Fiber 节点连接起来构成 Fiber 树
              workInProgress = performUnitOfWork(workInProgress);
          }
      }
      
    • 协调器和渲染器不再是交替工作,调度器将任务交给协调器后,协调器会给变化的虚拟 DOM 打上代表增、删、更新的标记;

      // https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactSideEffectTags.js
      export const Placement = /*                    */ 0b000000000000010;
      export const Update = /*                       */ 0b000000000000100;
      export const PlacementAndUpdate = /*           */ 0b000000000000110;
      export const Deletion = /*                     */ 0b000000000001000;
      
    • 调度器和协调器的工作都在内存中进行,只有当所有组件都完成协调器的工作,才会统一交给渲染器

  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上。

调度器和协调器都是平台无关的。

Fiber 架构

Fiber 也就是 React16 的虚拟 DOM,每个 Fiber 节点对应一个 React Element。

// https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiber.new.js#L117
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag; 			// 组件类型:Function,Class,Host(原生)
  this.key = key;			// key 属性
  this.elementType = null;
  this.type = null; 		// 对于 FunctionComponent 指函数本身;对于 ClassComponent 指 class;对于 HostComponent 指 DOM 节点 tagName

  this.stateNode = null;

  // 用于连接其他 Fiber 节点形成 Fiber 树
  this.return = null;		// 指向父级 Fiber 节点(指节点执行完 completeWork 后会返回的下一个节点,子 Fiber 节点及其兄弟节点完成工作后会返回其父级节点)
  this.child = null;		// 指向子 Fiber 节点
  this.sibling = null;		// 指向右边第一个兄弟 Fiber 节点
  this.index = 0;

  this.ref = null;

  // 保存本次更新造成的状态改变的相关信息
  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;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该 Fiber在另一次更新时对应的 Fiber
  this.alternate = null;
}

canvas 绘制动画的每一帧前都会调用 ctx.clearRect 清除上一帧的画面,如果当前帧画面计算量较大,清除掉上一帧画面到绘完制当前帧画面之间的间隔会比较长,导致出现白屏。解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,这种在内存中构建并直接替换的技术叫做双缓存

React 使用双缓存来完成 Fiber 树的构建与替换,对应着 DOM 树的创建与更新。

React 中最多会同时存在两棵 Fiber 树。当前屏幕上显示的内容对应的 Fiber 树称为 current Fiber 树,正在内存中构建的 Fiber 树称为 workInProgress Fiber 树,两者通过 alternate 属性连接。React 应用的根节点通过使 current 属性(指针)在不同 Fiber 树的 rootFiber 间切换来完成 current Fiber 树指向的切换。每次状态更新都会产生新的 workInProgress Fiber 树,通过 currentworkInProgress 的替换,完成 DOM 更新。

在构建 workInProgress Fiber 树时会尝试复用 current Fiber 树中已有的 Fiber 节点的属性,是否复用由 Diff 算法决定。

Reconciler 工作的阶段称为 render 阶段,该阶段会调用组件的 render 方法;Renderer 工作的阶段称为 commit 阶段,commit 阶段会把 render 阶段提交的信息渲染到页面上;render 与 commit 阶段统称为 work,如果任务正在 Scheduler 内调度,就不归到 work 范畴。

首次执行 ReactDOM.render 会创建 fiberRootNode 和 rootFiber。fiberRootNode 是整个应用的根节点,rootFiber 是应用根组件(如 <App />)所在组件树的根节点。
我们可以多次调用 ReactDOM.render 渲染不同的组件树,它们会拥有不同的 rootFiber,但整个应用的根节点只有一个,即 fiberRootNode。

JSX

JSX 在编译时会被 Babel 编译为 React.createElement 调用。
可以通过 @babel/plugin-transform-react-jsx 插件显式告诉 Babel 编译时将 JSX 编译成什么函数的调用(默认为 React.createElement)。

// https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react/src/ReactElement.js#L348
export function createElement(type, config, children) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    // 将 config 处理后赋值给 props
    // ...
  }

  const childrenLength = arguments.length - 2;
  // 处理 children,会被赋值给 props.children
  // ...

  // 处理 defaultProps
  // ...

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // 标记这是个 React Element
    $$typeof: REACT_ELEMENT_TYPE,

    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner,
  };

  return element;
};

// https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react/src/ReactElement.js#L547
export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

$$typeof === REACT_ELEMENT_TYPE 的非 null 对象就是一个合法的 React Element

在 React中我们使用 ClassComponentFunctionComponent 构建组件。React 通过 ClassComponent 实例原型上的 isReactComponent 变量判断是否是 ClassComponent

AppClass instanceof Function === true;
AppFunc instanceof Function === true;

JSX 是一种描述当前组件内容的数据结构,不包含组件 schedule、reconcile、render 所需的信息,比如组件在更新中的优先级、组件的 state、组件被打上的用在 Render 阶段的标记,这些都包含在 FiberNode 中。

组件 mount 时,Reconciler 根据 JSX 描述的组件内容生成组件对应的 Fiber 节点;组件 update 时,Reconciler 将 JSX 与 Fiber 节点保存的数据对比,生成组件对应的 Fiber 节点,并根据对比结果为 Fiber 节点打上标记。

流程

render 阶段

performUnitOfWork 执行深度优先遍历:

// https://github.com/facebook/react/blob/970fa122d8188bafa600e9b5214833487fbf1092/packages/react-reconciler/src/ReactFiberWorkLoop.new.js#L1599
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
function performUnitOfWork(fiber) {
  // 执行 beginWork,根据传入的当前 Fiber 节点 创建子 Fiber 节点
  // https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L3075

  if (fiber.child) {
    performUnitOfWork(fiber.child);
  }

  // 执行 completeWork

  if (fiber.sibling) {
    performUnitOfWork(fiber.sibling);
  }
}

performUnitOfWork 主要调用 beginWorkcompleteWork 方法。

beginWork:

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update:如果 current 存在可优化的路径,就直接复用 current(即上一次更新的 Fiber 节点)
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) { // 当前 Fiber 节点优先级不够
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        // ...
      }
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    } else {
      didReceiveUpdate = false;
    }
  } else {
    didReceiveUpdate = false;
  }

  // mount 时:根据 tag 不同,创建不同的子 Fiber 节点
  // 常见的组件类型(如 FunctionComponent,ClassComponent,HostComponent),最终会进入 reconcileChildren
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...
    case LazyComponent: 
      // ...
    case FunctionComponent: 
      // ...
    case ClassComponent: 
      // ...
    case HostRoot:
      // ...
    case HostComponent:
      // ...
    case HostText:
      // ...
    // ...
  }
}

// https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberBeginWork.new.js#L233
// 生成新的子 Fiber 节点并赋值给 workInProgress.child,作为本次 beginWork 返回值,
// 并作为下次执行 performUnitOfWork 的 workInProgress 参数
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // mount 的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // update 的组件(会为生成的 Fiber 节点带上 effectTag 属性)
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

Fiber 节点对应的 DOM 节点要被 Renderer 插入页面中要满足两个条件:

  • Fiber.stateNode 存在,即 Fiber 节点中保存了对应的 DOM 节点;
    • mount 时,fiber.stateNode === null 且没有 effectTag,这种首屏渲染的情况,fiber.stateNode 会在 completeWork 中创建。
  • (fiber.effectTag & Placement) !== 0,即 Fiber 节点存在 Placement effectTag。
    • mount 时只有 rootFiber 会赋值 Placement effectTag,在 commit 阶段只会执行一次插入操作(appendAllChildren)。

completeWork

// https://github.com/facebook/react/blob/1fb18e22ae66fdb1dc127347e169e73948778e5a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js#L673
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...
      return null;
    }
    case HostRoot: {
      // ...
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      popHostContext(workInProgress);
	  const rootContainerInstance = getRootHostContainer();
	  const type = workInProgress.type;

	  if (current !== null && workInProgress.stateNode != null) {
		// update
		// ...
		// Fiber 节点已经存在对应 DOM 节点,不需要生成 DOM 节点,需要做的主要是处理 props;
		// updateHostComponent 内部,处理完的 props 被赋值给 workInProgress.updateQueue,并最终在 commit 阶段被渲染在页面上。
		updateHostComponent(
		  current,
		  workInProgress,
		  type,
		  newProps,
		  rootContainerInstance,
		);
	  } else {
		// mount
		// ...
		const currentHostContext = getHostContext();
		// 为 Fiber 创建对应 DOM 节点
		const instance = createInstance(
		  type,
		  newProps,
		  rootContainerInstance,
		  currentHostContext,
		  workInProgress,
		);
		// 将子孙 DOM 节点插入刚生成的 DOM 节点中
		appendAllChildren(instance, workInProgress, false, false);
		// DOM 节点赋值给 fiber.stateNode
		workInProgress.stateNode = instance;

		// 处理 props
		if (
		  finalizeInitialChildren(
			instance,
			type,
			newProps,
			rootContainerInstance,
			currentHostContext,
		  )
		) {
		  markUpdate(workInProgress);
		}
	  }
	  return null;
    }
  // ...

作为 DOM 操作的依据,commit 阶段需要找到所有有 effectTag 的 Fiber 节点并依次执行 effectTag 对应的操作。completeUnitOfWork 中每个执行完 completeWork 且存在 effectTag 的 Fiber 节点会被保存在一条被称为 effectList 的单向链表中,effectList 中第一个 Fiber 节点保存在 fiber.firstEffect,最后一个元素保存在 fiber.lastEffect

                       nextEffect         nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber

至此 render 阶段全部工作完成,performSyncWorkOnRoot 中 fiberRootNode 被传递给 commitRoot,启动 commit 阶段工作流程。

commit 阶段


References