阅读图解React笔记1
2022 react本文仅仅是阅读 图解 React 原理系列 的笔记,了解更多内容请查看原文链接。
宏观介绍
React 下有 34 个包,Web 相关的核心包有 4 各,分别是 react,react-dom,react-reconciler 和 scheduler。
-
react
只提供了 react 组件的必要函数,需要和渲染器(react-dom)一并使用。
-
react-dom
react 渲染器之一,将 react-reconciler 的运行结果输出到 web 上。
- 引导 react 应用的启动(通过 ReactDOM.render)
- 实现 HostConfig 协议(ReactDOMHostConfig.js),能够将 react-reconciler 包构造出来的 fiber 树表现出来,生成 DOM 节点。
-
react-reconciler
react 调和机制,接受输入(shceduleUpdateOnFiber),将 fiber 树生成逻辑封装到一个回调函数(涉及 fiber 树形结构,fiber.updateQueue 队列,调和算法等)中。将此回调函数送入 scheduler 进行调度。
- 装载渲染器,保证在需要的时候,能够正确调用渲染器的 API,生成实际节点。
- 接收 react-dom 包 和 react 包发起的更新请求。
- 将 fiber 树的构造过程包装在一个回调函数中,将其传给 scheduler 等待调度。
-
scheduler
调度机制的实现,控制由 react-reconciler 送入的回调函数执行时机。
- 将 react-reconciler 提供的回调函数,包装到一个任务对象中。
- 在内部维护一个任务队列,优先级高的排在最前面。
- 循环消费任务队列,直到任务队列清空。
循环
在上面图中,可以看到两大循环,任务调度循环和 fiber 构建循环。
-
任务调度循环,源码位于 scheduler.js,控制所有任务的调度
-
Fiber 构建循环,源码位于 ReactFiberWorkLoop.js,控制整个 fiber 树的构造,整个过程是一个深度优先遍历。
区别与联系
两者的主要区别:
- 任务调度循环是以二叉堆为数据结构,循环执行堆的顶点,直到堆被清空
- 任务调度循环的逻辑偏向宏观,它调度的是每个任务,不需要关心任务具体是什么的。具体任务其实就是执行回调 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot
- fiber 构造循环是以树为数据结构,从上至下执行深度优先遍历
- fiber 构造循环的逻辑偏向于具体实现,是任务中一部分,只负责 fiber 树的构造
主干逻辑
通过上文的描述, 两大循环的分工可以总结为: 大循环(任务调度循环)负责调度 task, 小循环(fiber 构造循环)负责实现 task .
react 运行的主干逻辑, 即将输入转换为输出的核心步骤, 实际上就是围绕这两大工作循环进行展开.
结合上文的宏观概览图(展示核心包之间的调用关系), 可以将 react 运行的主干逻辑进行概括:
- 输入: 将每一次更新(如: 新增, 删除, 修改节点之后)视为一次更新需求(目的是要更新 DOM 节点).
- 注册调度任务: react-reconciler 收到更新需求之后, 并不会立即构造 fiber 树, 而是去调度中心 scheduler 注册一个新任务 task, 即把更新需求转换成一个 task.
- 执行调度任务(输出): 调度中心 scheduler 通过任务调度循环来执行 task(task 的执行过程又回到了 react-reconciler 包中).
- fiber 构造循环是 task 的实现环节之一, 循环完成之后会构造出最新的 fiber 树.
- commitRoot 是 task 的实现环节之二, 把最新的 fiber 树最终渲染到页面上, task 完成.
React
ReactElement
所有采用 jsx 语法书写的节点,都会被编译器转换,最终以 React.createElement(…) 的方式,创建出一个与之对应的 ReactElement 对象。
ReactElement 的类型定义在 shared 包的 ReactElementType.js 文件下。数据结构如下:
export type Source = {
fileName: string,
lineNumber: number,
};
export type ReactElement = {
$$typeof: any,
type: any,
key: any,
ref: any,
props: any,
// ReactFiber
_owner: any,
// __DEV__
_store: { validated: boolean, ... },
_self: React$Element<any>,
_shadowChildren: any,
_source: Source,
};
-
key 属性在
reconciler
阶段会用到, 目前只需要知道所有的ReactElement
对象都有 key 属性,默认值为 null。 -
type 属性决定了类型的种类:
-
它有三种类型的值:字符串(div, span 等 dom 节点),函数(function, class 等节点),react 内部定义的节点类型(portal, context, fragment 等)。它有 21 种(v18.2.0)内部定义的节点,可以在 shared/ReactSymbols.js 中查看。
-
在 reconciler 阶段,会根据 type 执行不同的逻辑:
- type 是一个字符串类型,则直接使用。
- type 是一个 ReactComponent 类型,则会调用其 render 方法获取子节点。
- type 是一个 function 类型,则会调用该方法获取子节点。
-
ReactComponent
ReactComponent 是 ReactElement 中的一种类型,是最经常使用的类组件。其位置位于 react/src/ReactBaseClasses.js 文件下。
ReactBaseClasses 提供了两个类组件,Component 和 PureComponent。
-
ReactComponent 是 class 类型,继承父类 Component,拥有特殊的方法(setState, forceUpdate)和特殊的属性(context,updater 等)
-
在 reconciler 阶段,会依据 ReactElement 对象的特征,生成对应的 fiber 节点。当识别到 ReactElement 对象是 class 类型的时候,会触发 ReactComponent 对象的生命周期,并调用其 render 方法,生成 ReactElement 子节点。
其他组件
function 组件也经常使用,Hook 只能在 function 类型的组件中使用。
如果 funciton 类型组件中没有使用 Hook,在 reconciler 阶段所有有关 Hook 的处理的都会被略过,最后调用该 function 拿到子节点 ReactElement。
如果使用了 Hook,逻辑就相对复杂,涉及到 Hook 的创建和状态保存。
React-reconciler
react-reconciler 是 react 应用的中枢,连接渲染器(react-dom) 和调度器(scheduler),同时本身也负责 fiber 树的构建。
要先知道 fiber 是核心,react 体系的渲染和更新都要以 fiber 作为数据模式,如果不能理解 fiber,也无法深入理解 react。
Fiber
Fiber 的类型定义位于 ReactInternalTypes.js 中:
exprot 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 | (((hanle: mixed) => void) & {_stringRef: ?string, ...}) | RefObj,
pendingProps: any,
memoizedProps: any,
dependencies: Dependencies | null,
mode: TypeOfMode,
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
lanes: Lanes,
childLanes: Lanes,
alternate: Fiber | null,
actualDuration?: number,
actualStartTime?: number,
selfBaseDuration?: number,
treeBaseDuration?: number,
}
相关说明:
- tag:fiber 类型,根据 ReactElement 组件的 type 进行生成,在 react 内部定义了 28 种(v18.13.0)类型。其位置位于 react-reconciler/src/ReactWorkTags.js
- key:与 ReactElement 组件的 key 一致。
- elementType:一般与 ReactElement 组件的 type 一致。
- type:一般与 elementType 属性一致。但是在开发环境下,为了兼容热更新,会对 function, class, ForwardRef 类型的 ReactElement 做一定的处理。具体可以查看 react-reconciler/src/ReactFiber.new.js
- stateNode:与 fiber 相关的局部状态节点。
- 如 HostComponent 类型指向 fiber 结点对应的 DOM 节点;
- 根结点的 stateNode 指向的是 FiberRoot;
- class 类型的节点 stateNode 指向的是 class 实例;
- return:指向父结点。
- child:指向第一个子节点。
- sibling:指向下一个兄弟结点。
- index:fiber 在兄弟结点中的索引,如果是单节点默认为 0。
- ref:指向在 ReactElement 组件上设置的 ref。
- pendingProps:输入属性,从 ReactElement 对象传入的 props。
- memoizedProps:上一次生成子节点用到的属性,生成子节点之后保持在内存中。
- updateQueue:存储 update 更新对象的队列,每次发起更新,都需要在该队列上创建一个 update 对象。
- memoizedState:上一次生成子节点之后保持在内存中的局部状态。
- dependencies:该 fiber 节点的依赖(context, events)等。
- mode:二进制 Bitfield,继承至父节点,影响本 fiber 节点及其子树中的所有结点。所在文件可以查看 react-reconciler/src/ReactTypeOfMode.js
- flags:标志位,副作用的标记。reconciler 阶段会将所拥有 flags 标记的节点添加到副作用链中,等待 commit 阶段的处理。flags 的定义文件位于 react-reconciler/src/ReactFiberFlags.js
- subtreeFlags:替代 16.x 版本中的 firstEffect, nextEffect,默认未开启。
- deletions:存储将要被删除的子节点。默认未开启。
- nextEffect:单向链表,指向下一个有副作用的 fiber 节点。
- firstEffect:指向副作用链表中的第一个 fiber 节点。
- lastEffect:指向副作用链表中的最后一个 fiber 节点。
- lanes:本 fiber 节点所属的优先级,创建 fiber 的时候设置。
- childLanes:子节点所属的优先级。
- alternate:指向内存中的另一个 fiber,每个被更新过的 fiber 节点在内存中都是成对出现(current 和 workInProgress)。
Update 与 UpdateQueue
在 fiber 对象中有一个属性 updateQueue,是一个链式队列。
UpdateQueue 类对象的数据结构:
export type Update<State> = {
eventTime: number,
lane: Lane,
tag: 0 | 1 | 2 | 3,
payload: any,
callback: (() => mixed) | null,
next: Update<State> | null,
};
export type SharedQueue<State> = {
pending: Update<State> | null,
lanes: Lanes,
hiddenCallbacks: Array<() => mixed> | null,
};
export type UpdateQueue<State> = {
baseState: State,
firstBaseUpdate: Update<State> | null,
lastBaseUpdate: Update<State> | null,
shared: SharedQueue<State>,
callbacks: Array<() => mixed> | null,
};
相关说明:
- Update
- eventTime: 发起 update 事件的时间
- lane: update 所属的优先级
- tag: 表示 update 种类,共有 UpdateState, ReplaceState, ForceUpdate, CaptureUpdate。
- payload: update 对象真正需要更新的数据,可以是回调函数或者对象。
- callback: 回调函数,commit 完成之后调用。
- next:指向链表中的下一个,由于 UpdateQueue 是一个环形链表,最后一个 update.next 指向第一个 update 对象。
- SharedQueue
- pending: 指向即将输入的 update 队列,在类组件中调用 setState() 之后,会将新的 udpate 对象添加到这个队列中来。
- lanes: 所属优先级
- hiddenCallbacks: 隐藏的回调函数
- SharedQueue
- baseState: 表示此队列的基础 state
- firstBaseUpdate: 指向基础队列的队首
- lastBaseUpdate: 指向基础队列的队尾
- shared: 共享队列
- effects: 用于保存有 callback 回调函数的 update 对象,在 commit 之后,会依次调用这里的回调函数 fiber
Hook
Hook 用于 function 组件中,能够保持 function 组件的状态。官方共定义了 17 种(v18.0.2) Hook 类型,文件位于 react-reconciler/src/ReactInternalTypes.js。
export type HookType =
| 'useState'
| 'useReducer'
| 'useContext'
| 'useRef'
| 'useEffect'
| 'useInsertionEffect'
| 'useLayoutEffect'
| 'useCallbak'
| 'useMemo'
| 'useImperativeHandle'
| 'useDebugValue'
| 'useTranstion'
| 'useMutableSource'
| 'useSyncExternalStore'
| 'useId'
| 'useCacheRefresh';
Hook 对象的数据结构,react-reconciler/src/ReactFiberHooks.js
export type Update<S, A> = {
lane: Lane,
aciton: A,
hasEagerState: boolean,
eagerState: S | null,
next: Update<S, A>,
};
export type UpdateQueue<S, A> = {
pending: Update<S, A> | null,
lanes: Lanes,
dispatch: ((A) => mixed) | null,
lastRenderedReducer: ((S, A) => S) | null,
lastRenderedState: S | null,
};
export type Hook = {
memorizedState: any,
baseState: any,
baseQueue: Update<any, any> | null,
queue: any,
next: Hook | null,
};
相关说明:
- Hook
- memoizedState: 内存状态,用于输出最终的 fiber 树
- baseState: 基础状态,当 Hook.queue 更新过后,baseState 也会更新
- baseQueue: 基础状态队列,在 reconciler 阶段会辅助状态合并
- queue: 指向一个 Update 队列
- next: 指向该 function 组件的下一个 Hook 对象,使得多个 Hook 之间也构成一个链表。
- Update 和 UpdateQueque (Hook.queue 和 Hook.baseQueue) 是为了保证 Hook 对象能够顺利更新,与上下文 fiber.updateQueque 中的 UpdateQueue 和 Update 是不一样的。
Hook 与 fiber 的关系: 在 fiber 对象中有一个属性 fiber.memoizedState 指向 fiber 节点的内存状态。在 function 类型的组件中,fiber.memoziedState 就指向 Hook 队列。所以 Hook 也不能脱离 fiber 而存在。
Scheduler
scheduler 负责调度,在内部维护一个任务队列 taskQueue,该队列是一个最小堆数组,其中存储了 task 对象。
Task
scheduler 包中,没有为 task 对象定义 type,其直接定义在 js 代码中,其文件位置 scheduler/src/forks/Scheduler.js。
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
expirationTime,
sortIndex: -1,
};
相关说明:
- id: 唯一标识
- callback: task 最核心的字段,指向 react-reconciler 所提供的回调函数。
- priorityLevel: 优先级
- startTime: 一个时间戳,代表 task 的开始时间
- expirationTime: 过期时间
- sortIndex: 控制 task 在队列中的次序,值越小越靠前
注意 task 中没有 next 属性,它不是一个链表,其顺序是通过堆排序来实现的。