React 核心包概览

October 12, 2022

react 是用于构建用户界面的 JavaScript 库

结构

React 仓库中与 web 开发相关的核心包只有四个

  • react: 在编写 React 应用时,引用的函数基本都来自这个包,提供了编写 React 组件必备的函数
  • scheduler: 调度机制的核心实现,负责调度不同优先级的任务
  • react-reconciler: 接受 react 包提供的函数调用的输入,生成任务由 scheduler 调度,最终产物交给 react-dom
  • react-dom: react 渲染器之一,负责将 react-reconciler 的产物输出到 web 界面中

React 包结构@2x.png

React

React 中包含所有全局 React API,比如:React.createElement

我们日常编写的 JSX 在编译时就会转化为 React.createElement 方法,这也是为什么在每个 JSX 的文件中你必须显示声明

import React from 'react'

React.createELement 最终会调用 ReactElement 方法返回一个包含组件数据的对象,也就是 React Element 对象

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;
};

React 核心只包含定义组件必要的 API。它不包含 Reconciler 算法或者其他平台特定的代码。它同时适用于 React DOM 和 React Native 组件

React Dom

服务于 Dom 的渲染器,作用就是将 React 组件渲染成 Dom

ReactDOM.render(<App />, document.getElementById('root'))

这行代码的意思就是将名为 App 的 React 组件渲染到 id 为 root 的根 dom 节点中去,此时 React DOM 拿到所有 ReactElements 就会去交给 reconciler ,最后拿到 reconciler 的输出再负责最后的渲染,因此 React Dom 在此起到了一个连接点的作用。

Fiber 树的渲染可以划分为三阶段,即渲染前,渲染,渲染后。想知道 Fiber 树如何构建的可以先移步下面的 React Reconciler

渲染前

这一阶段主要是为真正的渲染做一些准备

  1. 处理 blur 和 focus 相关逻辑
  2. 设置全局状态
  3. 重置全局变量
  4. 调用该阶段生命周期方法,例如getSnapshotBeforeUpdate
  5. 调度 useEffect (注意是调度,不是调用)

渲染

该阶段将发生真正的 Dom 操作

  1. 根据 effectList 对 Dom 进行增删改
  2. 对 fiber 进行删除时,解绑 ref,类组件调用 componetWillUnmount 方法
  3. 函数式组件调用 useLayoutEffect 的销毁函数
  4. 同步调用 componentDidMount 和 useLayoutEffect 等,此时浏览器还没开始绘制

渲染后

浏览器渲染完毕后,浏览器通知 React 自己处于空闲状态。此阶段已经可以访问真实的 Dom

  1. 调用 useEffect
  2. 数据清除操作

React Reconciler

reconciler 运作流程有四个步骤,目的就是产出一种稳定结构供渲染器渲染

  1. 输入函数 scheduleUpdateOnFiber,只要涉及到需要改变 fiber 的操作(无论是首次渲染 或后续更新操作), 最后都会间接调用scheduleUpdateOnFiber
  2. 与 scheduler 交互,注册调度任务(其中涉及优先级管理、时间切片等复杂逻辑
  3. 执行任务回调,在内存中构造 Fiber 树
  4. 输出结果提供给渲染器进行渲染

Fiber 树构建

Fiber 树的构建分为两种情况,初次构建与对比更新

初次构建

入口:ReactDom.render → updateContainer →scheduleUpdateOnFiber

对比更新

入口:

  1. Class组件中调用setState
  2. Function组件中调用hook对象暴露出的dispatchAction
  3. 在container节点上重复调用render(官网示例)

当发生更新时,创建新 Fiber 之前需要和旧 Fiber 进行对比。最后构造的 Fiber 树有可能是全新的, 也可能是部分更新的。

reconciler 的主要内容就是 fiber 树的构建,而 Fiber Reconciler 是从 Stack Reconciler 重构而来,重点的优化就是实现了可中断的的递归遍历

// performSyncWorkOnRoot 会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot 会调用该方法
function workLoopConcurrent() {
	// shouldYield 可暂停循环
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

在 Fiber Reconciler 中,performUnitOfWork 分为两个阶段,探寻阶段 beginWork 与回溯阶段 completeWork,以上两个阶段是每个 Fiber 节点都会经历,也正是这两个阶段完成了 Fiber 树的构建

beginWork

从根节点开始深度优先遍历,对遍历到的 Fiber 节点执行 beginWork,整体流程大致如下图所示

beginWork@2x.png

beginWork 会根据 ReactElement 对象创建所有的 Fiber 节点,最终构造出 Fiber 树的整体结构(通过设置 return 与 sibling ),在 Update 阶段时会确立好要更新的 Fiber 节点的方式存储到 effectTag 中,便于 render 阶段时处理。

你可以从这里 (opens new window)看到effectTag对应的DOM操作,比如:

// DOM需要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM需要更新
export const Update = /*                   */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM需要删除
export const Deletion = /*                 */ 0b00000000001000;

completeWork

从叶子结点开始向上回溯,对 Fiber 节点执行 completeWork 方法。beginWork 会创建完 Fiber 节点并且将对该节点的改动添加到该节点的 effectTag 中,但在渲染阶段我们是要将其转化为 Dom 节点并执行 Dom 操作的,所以在 completeWork 阶段将会为渲染层准备好这些数据

  1. 生成当前节点的 Dom 节点,将子孙的 Dom 节点插入到当前节点,设置 Fiber.stateNode 指向当前的 Dom 实例
  2. 为 Dom 节点设置属性,绑定事件
  3. 将 beginWork 设置的 effectTag 设置到父节点的 effectList 中

通过上述几个步骤,要生成的 Dom 树与要执行的副作用变都被回溯到了根节点上,commit 阶段时便不需要再深度优先遍历去确认

至此,fiber 树构建完成,只需要将根节点 FiberRootNode 交给 commit 阶段的方法即可。也就是 React-dom 中的渲染

reconciler 本身不与 DOM 绑定。挂载的确切结果(在源代码中有时叫做 “挂载映像”)取决于 renderer,可以是一个 DOM 节点(React DOM),一个字符串(React DOM Server),或是一个表示原生视图的数字(React Native)。

Scheduler

React 内部对于优先级的管理体系。

可中断渲染、时间切片、异步渲染等特性,都依赖 Scheduler

调度内核

Scheduler 需要满足以下两个功能

  1. 暂停 Js 执行,将主线程还给浏览器,让浏览器有机会更新页面
  2. 在未来某个时刻继续调度任务,执行上次还未执行完的任务

scheduler@2x.png

微任务会在一次事件循环的最后执行,并不能让出主线程给浏览器,因此只能用宏任务去实现,这里的 MessageChannel 的作用就是生成宏任务( setTimeout 也可以生成宏任务,因为 setTimeout 并不稳定,嵌套调用时存在最小延迟 4ms

任务队列

调度的目的是消费任务,scheduler 中维护了一个 taskQueue,任务队列的管理便是围绕这个 taskQueue 来展开

// Tasks are stored on a min heap
var taskQueue = []

创建任务

将任务创建完成后添加到任务队列中,然后触发调度申请

// 省略部分无关代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 1. 获取当前时间
  var currentTime = getCurrentTime();
  var startTime;
  if (typeof options === 'object' && options !== null) {
    // 从函数调用关系来看, 在v17.0.2中,所有调用 unstable_scheduleCallback 都未传入options
    // 所以省略延时任务相关的代码
  } else {
    startTime = currentTime;
  }
  // 2. 根据传入的优先级, 设置任务的过期时间 expirationTime
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }
  var expirationTime = startTime + timeout;
  // 3. 创建新任务
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (startTime > currentTime) {
    // 省略无关代码 v17.0.2中不会使用
  } else {
    newTask.sortIndex = expirationTime;
    // 4. 加入任务队列
    push(taskQueue, newTask);
    // 5. 请求调度
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }
  return newTask;
}

消费任务

收到调度申请后开始循环处理任务队列中的任务

// 省略无关代码
function flushWork(hasTimeRemaining, initialTime) {
  // 1. 做好全局标记, 表示现在已经进入调度阶段
  isHostCallbackScheduled = false;
  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    // 2. 循环消费队列
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    // 3. 还原全局标记
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}

主要逻辑都在 workLoop 中

// 省略部分无关代码
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime; // 保存当前时间, 用于判断任务是否过期
  currentTask = peek(taskQueue); // 获取队列中的第一个任务
  while (currentTask !== null) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 虽然currentTask没有过期, 但是执行时间超过了限制(毕竟只有5ms, shouldYieldToHost()返回true). 停止继续执行, 让出主线程
      break;
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // 执行回调
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      // 回调完成, 判断是否还有连续(派生)回调
      if (typeof continuationCallback === 'function') {
        // 产生了连续回调(如fiber树太大, 出现了中断渲染), 保留currentTask
        currentTask.callback = continuationCallback;
      } else {
        // 把currentTask移出队列
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
    } else {
      // 如果任务被取消(这时currentTask.callback = null), 将其移出队列
      pop(taskQueue);
    }
    // 更新currentTask
    currentTask = peek(taskQueue);
  }
  if (currentTask !== null) {
    return true; // 如果task队列没有清空, 返回true. 等待调度中心下一次回调
  } else {
    return false; // task队列已经清空, 返回false.
  }
}

每一次 while 循环的退出就是一个时间切片,退出条件有两个

  1. 没有更多任务了
  2. 在消费任务队列时,执行下一个 task 的 callback 之前,都会进行检测,所以检测是以 task 为单位

参考链接

React 官方中文文档 - 用于构建用户界面的 JavaScript 库

图解React原理系列 - 图解React

React技术揭秘