这是我参与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 个属性:
-
key
属性在reconciler
阶段会用到, 目前只需要知道所有的ReactElement
对象都有 key 属性(且其默认值是 null, 这点十分重要, 在 diff 算法中会使用到). -
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
函数的第一个参数将作为创建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 类型, 自有它的特殊性(可对照源码, 更容易理解).
ReactComponent
是 class 类型, 继承父类Component
, 拥有特殊的方法(setState
,forceUpdate
)和特殊的属性(context
,updater
等).- 在
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 源码之架构总览中介绍过, 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
树对照起来:
注意:
- 这里的
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,
|};
属性解释:
-
UpdateQueue
baseState
: 表示此队列的基础 statefirstBaseUpdate
: 指向基础队列的队首lastBaseUpdate
: 指向基础队列的队尾shared
: 共享队列effects
: 用于保存有callback
回调函数的 update 对象, 在commit
之后, 会依次调用这里的回调函数.
-
SharedQueue
pending
: 指向即将输入的update
队列. 在class
组件中调用setState()
之后, 会将新的 update 对象添加到这个队列中来.
-
Update
eventTime
: 发起update
事件的时间(17.0.2 中作为临时字段, 即将移出)lane
:update
所属的优先级tag
: 表示update
种类, 共 4 种. UpdateState,ReplaceState,ForceUpdate,CaptureUpdatepayload
: 载荷,update
对象真正需要更新的数据, 可以设置成一个回调函数或者对象.callback
: 回调函数.commit
完成之后会调用.next
: 指向链表中的下一个, 由于UpdateQueue
是一个环形链表, 最后一个update.next
指向第一个update
对象.
updateQueue
是fiber
对象的一个属性, 所以不能脱离fiber
存在. 它们之间数据结构和引用关系如下:
注意:
- 此处只是展示数据结构和引用关系.对于
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,
|};
属性解释:
Hook
memoizedState
: 内存状态, 用于输出给形成最终的fiber
树baseState
: 基础状态, 当Hook.queue
更新过后,baseState
也会更新.baseQueue
: 基础状态队列, 在reconciler
阶段会辅助状态合并.queue
: 指向一个Update
队列next
: 指向该function
组件的下一个Hook
对象, 使得多个Hook
之间也构成了一个链表.
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 源码的深入分析. 在后续对整个运行核心的解读中会多次引用到这些对象, 并对其在运行时的具体作用深入解读.
写在最后
本文属于图解react源码系列中的基础板块, 本系列近 20 篇文章,真的是为了搞懂React源码, 进而提升架构和编码能力.
目前图解部分初稿
已经全部完成, 将在8月全部更新, 如文章有表述错误, 会在github第一时间修正.