图解React图解React
  • 原理解析
  • 高频算法
  • 面试题
⌘ K
基本概念
宏观包结构
两大工作循环
高频对象
运行核心
启动过程
reconciler 运作流程
优先级管理
调度原理
fiber 树构造(基础准备)
fiber 树构造(初次创建)
fiber 树构造(对比更新)
fiber 树渲染
状态管理
状态与副作用
Hook 原理(概览)
Hook 原理(状态Hook)
Hook 原理(副作用Hook)
context 原理
交互
合成事件
Copyright © 2023 | Powered by dumi

React 应用中的高频对象

在 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 节点), 函数(代表function, 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 中的 function), 可以更直观的看出调用逻辑.

createElement函数的第一个参数将作为创建ReactElement的type. 可以看到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阶段完成之后才会形成完整的结构).

注意:

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

react-reconciler 包

在宏观结构中介绍过, 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用于下一次比较.pendingProps和memoizedProps比较可以得出属性是否变动.
  • 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树对照起来:

reactelementfiber
注意:
  • 这里的fiber树只是为了和上文中的ReactElement树对照, 所以只用观察红色虚线框内的节点. 根节点HostRootFiber在react 应用的启动模式章节中详细解读.
  • 其中<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对象.

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

注意:

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

Hook 对象

Hook用于function组件中, 能够保持function组件的状态(与class组件中的state在性质上是相同的, 都是为了保持组件的状态).在[email protected]以后, 官方开始推荐使用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. Hook.queue和 Hook.baseQueue(即UpdateQueue和Update)是为了保证Hook对象能够顺利更新, 与上文fiber.updateQueue中的UpdateQueue和Update是不一样的(且它们在不同的文件), 其逻辑会在状态组件(class 与 function)章节中详细解读.

Hook与fiber的关系:

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

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

注意:

  • 此处只是展示数据结构和引用关系.对于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对象优先级最高).

总结

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