图解React源码 - React 应用中的高频对象

613

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

react 应用中, 有很多特定的对象或数据结构. 了解这些内部的设计, 可以更容易理解 react 运行原理. 本章主要列举从 react 启动到渲染过程出现频率较高, 影响范围较大的对象, 它们贯穿整个 react 运行时.

其他过程的重要对象:

  • 事件对象(位于react-dom/events保障 react 应用能够响应 ui 交互), 在事件机制章节中详细解读.
  • ReactContext, ReactProvider, ReactConsumer对象, 在 context 机制章节中详细解读.

react 包

React 源码之架构总览中介绍过, 此包定义 react 组件(ReactElement)的必要函数, 提供一些操作ReactElement对象的 api.

所以这个包的核心需要理解ReactElement对象, 假设有如下入口函数:

// 入口函数
ReactDOM.render(<App />, document.getElementById('root'));

可以简单的认为, 包括<App/>及其所有子节点都是ReactElement对象(在 render 之后才会生成子节点, 后文详细解读), 每个ReactElement对象的区别在于 type 不同.

ReactElement 对象

其 type 定义在shared包中.

所有采用jsx语法书写的节点, 都会被编译器转换, 最终会以React.createElement(...)的方式, 创建出来一个与之对应的ReactElement对象.

ReactElement对象的数据结构如下:

export type ReactElement = {|
  // 用于辨别ReactElement对象
  $$typeof: any,

  // 内部属性
  type: any, // 表明其种类
  key: any,
  ref: any,
  props: any,

  // ReactFiber 记录创建本对象的Fiber节点, 还未与Fiber树关联之前, 该属性为null
  _owner: any,

  // __DEV__ dev环境下的一些额外信息, 如文件路径, 文件名, 行列信息等
  _store: {validated: boolean, ...},
  _self: React$Element<any>,
  _shadowChildren: any,
  _source: Source,
|};

需要特别注意 2 个属性:

  1. key属性在reconciler阶段会用到, 目前只需要知道所有的ReactElement对象都有 key 属性(且其默认值是 null, 这点十分重要, 在 diff 算法中会使用到).

  2. type属性决定了节点的种类:

  • 它的值可以是字符串(代表div,span等 dom 节点), 函数(代表fuction, class等节点), 或者 react 内部定义的节点类型(portal,context,fragment等)
  • reconciler阶段, 会根据 type 执行不同的逻辑(在 fiber 构建阶段详细解读).
    • 如 type 是一个字符串类型, 则直接使用.
    • 如 type 是一个ReactComponent类型, 则会调用其 render 方法获取子节点.
    • 如 type 是一个function类型,则会调用该方法获取子节点
    • ...

v17.0.2中, 定义了 20 种内部节点类型. 根据运行时环境不同, 分别采用 16 进制的字面量和Symbol进行表示.

ReactComponent对象

对于ReactElement来讲, ReactComponent仅仅是诸多type类型中的一种.

对于开发者来讲, ReactComponent使用非常高频(在状态组件章节中详细解读), 在本节只是先证明它只是一种特殊的ReactElement.

这里用一个简单的示例, 通过查看编译后的代码来说明

class App extends React.Component {
  render() {
    return (
      <div className="app">
        <header>header</header>
        <Content />
        <footer>footer</footer>
      </div>
    );
  }
}

class Content extends React.Component {
  render() {
    return (
      <React.Fragment>
        <p>1</p>
        <p>2</p>
        <p>3</p>
      </React.Fragment>
    );
  }
}

export default App;

编译之后的代码(此处只编译了 jsx 语法, 并没有将 class 语法编译成 es5 中的 fuction), 可以更直观的看出调用逻辑.

createElement函数的第一个参数将作为创建ReactElementtype. 可以看到Content这个变量被编译器命名为App_Content, 并作为第一个参数(引用传递), 传入了createElement.

class App_App extends react_default.a.Component {
  render() {
    return /*#__PURE__*/ react_default.a.createElement(
      'div',
      {
        className: 'app',
      } /*#__PURE__*/,
      react_default.a.createElement('header', null, 'header') /*#__PURE__*/,

      // 此处直接将Content传入, 是一个指针传递
      react_default.a.createElement(App_Content, null) /*#__PURE__*/,
      react_default.a.createElement('footer', null, 'footer'),
    );
  }
}
class App_Content extends react_default.a.Component {
  render() {
    return /*#__PURE__*/ react_default.a.createElement(
      react_default.a.Fragment,
      null /*#__PURE__*/,
      react_default.a.createElement('p', null, '1'),
      /*#__PURE__*/

      react_default.a.createElement('p', null, '2'),
      /*#__PURE__*/

      react_default.a.createElement('p', null, '3'),
    );
  }
}

上述示例演示了ReactComponent是诸多ReactElement种类中的一种情况, 但是由于ReactComponent是 class 类型, 自有它的特殊性(可对照源码, 更容易理解).

  1. ReactComponent是 class 类型, 继承父类Component, 拥有特殊的方法(setState,forceUpdate)和特殊的属性(context,updater等).
  2. reconciler阶段, 会依据ReactElement对象的特征, 生成对应的 fiber 节点. 当识别到ReactElement对象是 class 类型的时候, 会触发ReactComponent 对象的生命周期, 并调用其 render方法, 生成ReactElement子节点.

其他 ReactElement

上文介绍了第一种特殊的ReactElement(class类型的组件), 除此之外function类型的组件也需要深入了解, 因为Hook只能在function类型的组件中使用.

如果在function类型的组件中没有使用Hook(如: useState, useEffect等), 在reconciler阶段所有有关Hook的处理都会略过, 最后调用该function拿到子节点ReactElement.

如果使用了Hook, 逻辑就相对复杂, 涉及到Hook创建和状态保存(有关 Hook 的原理部分, 在 Hook 原理章节中详细解读). 此处只需要了解function类型的组件和class类型的组件一样, 是诸多ReactElement形式中的一种.

ReactElement内存结构

通过前文对ReactElement的介绍, 可以比较容易的画出<App/>这个ReactElement对象在内存中的结构(reconciler阶段完成之后才会形成完整的结构).

注意:

  • classfunction类型的组件,其子节点是在 render 之后(reconciler阶段)才生成的. 此处只是单独表示ReactElement的数据结构.
  • 父级对象和子级对象之间是通过props.children属性进行关联的(与 fiber 树不同).
  • ReactElement虽然不能算是一个严格的树, 也不能算是一个严格的链表. 它的生成过程是至顶向下的, 是所有组件节点的总和.
  • ReactElement树(暂且用树来表述)和fiber树是以props.children为单位先后交替生成的(在 fiber 树构建章节详细解读), 当ReactElement树构造完毕, fiber 树也随后构造完毕.
  • reconciler阶段会根据ReactElement的类型生成对应的fiber节点(不是一一对应, 比如Fragment类型的组件在生成fiber节点的时候会略过).

react-reconciler

React 源码之架构总览中介绍过, react-reconciler包是react应用的中枢, 连接渲染器(react-dom)和调度中心(scheduler), 同时自身也负责 fiber 树的构造.

对于此包的深入分析, 放在fiber 树构建, reconciler 工作空间等章节中.

此处先要知道fiber是核心, react 体系的渲染和更新都要以 fiber 作为数据模型, 如果不能理解 fiber, 也无法深入理解 react.

本章先预览一下此包中与fiber对象关联度较高的对象.

Fiber 对象

先看数据结构, 其 type 类型的定义在ReactInternalTypes.js中:

// 一个Fiber对象代表一个即将渲染或者已经渲染的组件(ReactElement), 一个组件可能对应多个fiber(current和WorkInProgress)
// 单个属性的解释在后文(在注释中无法添加超链接)
export type Fiber = {|
  tag: WorkTag,
  key: null | string,
  elementType: any,
  type: any,
  stateNode: any,
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,
  ref:
    | null
    | (((handle: mixed) => void) & { _stringRef: ?string, ... })
    | RefObject,
  pendingProps: any, // 从`ReactElement`对象传入的 props. 用于和`fiber.memoizedProps`比较可以得出属性是否变动
  memoizedProps: any, // 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中
  updateQueue: mixed, // 存储state更新的队列, 当前节点的state改动之后, 都会创建一个update对象添加到这个队列中.
  memoizedState: any, // 用于输出的state, 最终渲染所使用的state
  dependencies: Dependencies | null, // 该fiber节点所依赖的(contexts, events)等
  mode: TypeOfMode, // 二进制位Bitfield,继承至父节点,影响本fiber节点及其子树中所有节点. 与react应用的运行模式有关(有ConcurrentMode, BlockingMode, NoMode等选项).

  // Effect 副作用相关
  flags: Flags, // 标志位
  subtreeFlags: Flags, //替代16.x版本中的 firstEffect, nextEffect. 当设置了 enableNewReconciler=true才会启用
  deletions: Array<Fiber> | null, // 存储将要被删除的子节点. 当设置了 enableNewReconciler=true才会启用

  nextEffect: Fiber | null, // 单向链表, 指向下一个有副作用的fiber节点
  firstEffect: Fiber | null, // 指向副作用链表中的第一个fiber节点
  lastEffect: Fiber | null, // 指向副作用链表中的最后一个fiber节点

  // 优先级相关
  lanes: Lanes, // 本fiber节点的优先级
  childLanes: Lanes, // 子节点的优先级
  alternate: Fiber | null, // 指向内存中的另一个fiber, 每个被更新过fiber节点在内存中都是成对出现(current和workInProgress)

  // 性能统计相关(开启enableProfilerTimer后才会统计)
  // react-dev-tool会根据这些时间统计来评估性能
  actualDuration?: number, // 本次更新过程, 本节点以及子树所消耗的总时间
  actualStartTime?: number, // 标记本fiber节点开始构建的时间
  selfBaseDuration?: number, // 用于最近一次生成本fiber节点所消耗的实现
  treeBaseDuration?: number, // 生成子树所消耗的时间的总和
|};

属性解释:

  • fiber.tag: 表示 fiber 类型, 根据ReactElement组件的 type 进行生成, 在 react 内部共定义了25 种 tag.
  • fiber.key: 和ReactElement组件的 key 一致.
  • fiber.elementType: 一般来讲和ReactElement组件的 type 一致
  • fiber.type: 一般来讲和fiber.elementType一致. 一些特殊情形下, 比如在开发环境下为了兼容热更新(HotReloading), 会对function, class, ForwardRef类型的ReactElement做一定的处理, 这种情况会区别于fiber.elementType, 具体赋值关系可以查看源文件.
  • fiber.stateNode: 与fiber关联的局部状态节点(比如: HostComponent类型指向与fiber节点对应的 dom 节点; 根节点fiber.stateNode指向的是FiberRoot; class 类型节点其stateNode指向的是 class 实例).
  • fiber.return: 指向父节点.
  • fiber.child: 指向第一个子节点.
  • fiber.sibling: 指向下一个兄弟节点.
  • fiber.index: fiber 在兄弟节点中的索引, 如果是单节点默认为 0.
  • fiber.ref: 指向在ReactElement组件上设置的 ref(string类型的ref除外, 这种类型的ref已经不推荐使用, reconciler阶段会将string类型的ref转换成一个function类型).
  • fiber.pendingProps: 输入属性, 从ReactElement对象传入的 props. 用于和fiber.memoizedProps比较可以得出属性是否变动.
  • fiber.memoizedProps: 上一次生成子节点时用到的属性, 生成子节点之后保持在内存中. 向下生成子节点之前叫做pendingProps, 生成子节点之后会把pendingProps赋值给memoizedProps用于下一次比较.pendingPropsmemoizedProps比较可以得出属性是否变动.
  • fiber.updateQueue: 存储update更新对象的队列, 每一次发起更新, 都需要在该队列上创建一个update对象.
  • fiber.memoizedState: 上一次生成子节点之后保持在内存中的局部状态.
  • fiber.dependencies: 该 fiber 节点所依赖的(contexts, events)等, 在context机制章节详细说明.
  • fiber.mode: 二进制位 Bitfield,继承至父节点,影响本 fiber 节点及其子树中所有节点. 与 react 应用的运行模式有关(有 ConcurrentMode, BlockingMode, NoMode 等选项).
  • fiber.flags: 标志位, 副作用标记(在 16.x 版本中叫做effectTag, 相应pr), 在ReactFiberFlags.js中定义了所有的标志位. reconciler阶段会将所有拥有flags标记的节点添加到副作用链表中, 等待 commit 阶段的处理.
  • fiber.subtreeFlags: 替代 16.x 版本中的 firstEffect, nextEffect. 默认未开启, 当设置了enableNewReconciler=true 才会启用, 本系列只跟踪稳定版的代码, 未来版本不会深入解读, 使用示例见源码.
  • fiber.deletions: 存储将要被删除的子节点. 默认未开启, 当设置了enableNewReconciler=true 才会启用, 本系列只跟踪稳定版的代码, 未来版本不会深入解读, 使用示例见源码.
  • fiber.nextEffect: 单向链表, 指向下一个有副作用的 fiber 节点.
  • fiber.firstEffect: 指向副作用链表中的第一个 fiber 节点.
  • fiber.lastEffect: 指向副作用链表中的最后一个 fiber 节点.
  • fiber.lanes: 本 fiber 节点所属的优先级, 创建 fiber 的时候设置.
  • fiber.childLanes: 子节点所属的优先级.
  • fiber.alternate: 指向内存中的另一个 fiber, 每个被更新过 fiber 节点在内存中都是成对出现(current 和 workInProgress)

通过以上 25 个属性的解释, 对fiber对象有一个初步的认识.

最后绘制一颗 fiber 树与上文中的ReactElement树对照起来:

reactelement fiber

注意:

  • 这里的fiber树只是为了和上文中的ReactElement树对照, 所以只用观察红色虚线框内的节点. 根节点HostRootFiberreact 应用的启动模式章节中详细解读.
  • 其中<App/>,<Content/>ClassComponent类型的fiber节点, 其余节点都是普通HostComponent类型节点.
  • <Content/>的子节点在ReactElement树中是React.Fragment, 但是在fiber树中React.Fragment并没有与之对应的fiber节点(reconciler阶段对此类型节点做了单独处理, 所以ReactElement节点和fiber节点不是一对一匹配).

Update 与 UpdateQueue 对象

fiber对象中有一个属性fiber.updateQueue, 是一个链式队列(即使用链表实现的队列存储结构), 后文会根据场景表述成链表或队列.

首先观察Update对象的数据结构(对照源码):

export type Update<State> = {|
  eventTime: number, // 发起update事件的时间(17.0.2中作为临时字段, 即将移出)
  lane: Lane, // update所属的优先级

  tag: 0 | 1 | 2 | 3, //
  payload: any, // 载荷, 根据场景可以设置成一个回调函数或者对象
  callback: (() => mixed) | null, // 回调函数

  next: Update<State> | null, // 指向链表中的下一个, 由于UpdateQueue是一个环形链表, 最后一个update.next指向第一个update对象
|};

// =============== UpdateQueue ==============
type SharedQueue<State> = {|
  pending: Update<State> | null,
|};

export type UpdateQueue<State> = {|
  baseState: State,
  firstBaseUpdate: Update<State> | null,
  lastBaseUpdate: Update<State> | null,
  shared: SharedQueue<State>,
  effects: Array<Update<State>> | null,
|};

属性解释:

  1. UpdateQueue

    • baseState: 表示此队列的基础 state
    • firstBaseUpdate: 指向基础队列的队首
    • lastBaseUpdate: 指向基础队列的队尾
    • shared: 共享队列
    • effects: 用于保存有callback回调函数的 update 对象, 在commit之后, 会依次调用这里的回调函数.
  2. SharedQueue

    • pending: 指向即将输入的update队列. 在class组件中调用setState()之后, 会将新的 update 对象添加到这个队列中来.
  3. Update

    • eventTime: 发起update事件的时间(17.0.2 中作为临时字段, 即将移出)
    • lane: update所属的优先级
    • tag: 表示update种类, 共 4 种. UpdateState,ReplaceState,ForceUpdate,CaptureUpdate
    • payload: 载荷, update对象真正需要更新的数据, 可以设置成一个回调函数或者对象.
    • callback: 回调函数. commit完成之后会调用.
    • next: 指向链表中的下一个, 由于UpdateQueue是一个环形链表, 最后一个update.next指向第一个update对象.

updateQueuefiber对象的一个属性, 所以不能脱离fiber存在. 它们之间数据结构和引用关系如下:

updatequeue.png

注意:

  • 此处只是展示数据结构和引用关系.对于updateQueue在更新阶段的实际作用和运行逻辑, 会在状态组件(class 与 function)章节中详细解读.

Hook 对象

Hook用于function组件中, 能够保持function组件的状态(与class组件中的state在性质上是相同的, 都是为了保持组件的状态).在react@16.8以后, 官方开始推荐使用Hook语法, 常用的 api 有useState,useEffect,useCallback等, 官方一共定义了14 种Hook类型.

这些 api 背后都会创建一个Hook对象, 先观察Hook对象的数据结构:

export type Hook = {|
  memoizedState: any,
  baseState: any,
  baseQueue: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,
|};

type Update<S, A> = {|
  lane: Lane,
  action: A,
  eagerReducer: ((S, A) => S) | null,
  eagerState: S | null,
  next: Update<S, A>,
  priority?: ReactPriorityLevel,
|};

type UpdateQueue<S, A> = {|
  pending: Update<S, A> | null,
  dispatch: (A => mixed) | null,
  lastRenderedReducer: ((S, A) => S) | null,
  lastRenderedState: S | null,
|};

属性解释:

  1. Hook
  • memoizedState: 内存状态, 用于输出给形成最终的fiber
  • baseState: 基础状态, 当Hook.queue更新过后, baseState也会更新.
  • baseQueue: 基础状态队列, 在reconciler阶段会辅助状态合并.
  • queue: 指向一个Update队列
  • next: 指向该function组件的下一个Hook对象, 使得多个Hook之间也构成了一个链表.
  1. UpdateQueueUpdate是为了保证Hook对象能够顺利更新, 与上文fiber.updateQueue中的UpdateQueue和Update是不一样的(且它们在不同的文件), 其逻辑会在状态组件(class 与 function)章节中详细解读.

Hookfiber的关系:

fiber对象中有一个属性fiber.memoizedState指向fiber节点的内存状态. 在function类型的组件中, fiber.memoizedState就指向Hook队列(Hook队列保存了function类型的组件状态).

所以Hook也不能脱离fiber而存在, 它们之间的引用关系如下:

fiber-hook.png

注意:

  • 此处只是展示数据结构和引用关系.对于Hook在运行时的实际作用和逻辑, 会在状态组件(class 与 function)章节中详细解读.

scheduler 包

宏观结构中所介绍, scheduler包负责调度, 在内部维护一个任务队列(taskQueue). 这个队列是一个最小堆数组(详见React 算法之堆排序), 其中存储了 task 对象.

Task 对象

scheduler包中, 没有为 task 对象定义 type, 其定义是直接在 js 代码中:

var newTask = {
  id: taskIdCounter++,
  callback,
  priorityLevel,
  startTime,
  expirationTime,
  sortIndex: -1,
};

属性解释:

  • id: 位移标识
  • callback: task 最核心的字段, 指向react-reconciler包所提供的回调函数.
  • priorityLevel: 优先级
  • startTime: 一个时间戳,代表 task 的开始时间(创建时间 + 延时时间).
  • expirationTime: 过期时间.
  • sortIndex: 控制 task 在队列中的次序, 值越小的越靠前.

注意task中没有next属性, 它不是一个链表, 其顺序是通过堆排序来实现的(小顶堆数组, 始终保证数组中的第一个task对象优先级最高).

taskqueue.png

总结

本章主要浏览了 react 运行链路中出现的高频对象, 并对它们的数据结构做出了单独解释. 提前了解这些对象的数据结构, 更加有利于之后对 react 源码的深入分析. 在后续对整个运行核心的解读中会多次引用到这些对象, 并对其在运行时的具体作用深入解读.

写在最后

本文属于图解react源码系列中的基础板块, 本系列近 20 篇文章,真的是为了搞懂React源码, 进而提升架构和编码能力.

目前图解部分初稿已经全部完成, 将在8月全部更新, 如文章有表述错误, 会在github第一时间修正.