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 组件渲染到 idroot 的根 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. 同步调用 componentDidMountuseLayoutEffect 等,此时浏览器还没开始绘制

渲染后

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

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

React Reconciler

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

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

Fiber 树构建

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

初次构建

入口:ReactDom.renderupdateContainerscheduleUpdateOnFiber

对比更新

入口:

  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 树的整体结构(通过设置 returnsibling ),在 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技术揭秘