基础包结构
react
提供编写应用需要的api。
【使用场景】class组件中的
setState(),function组件中的各种hook
react-dom
react渲染器,将react和web平台(浏览器、nodejs)连接起来,向web界面输出react-reconciler的运行结果,即表现fiber树,生成dom节点。
【使用场景】
ReactDOM.render(<App/>, document.getElementByID('root'))引导react应用启动
scheduler
调度机制的核心实现,主要任务为执行回调,通过控制回调的执行时机,达到任务分片的目的,从而实现中断渲染。
react-reconciler
协调其他三个包的配合与调用,管理react应用状态输入和结果输出,最终传递给渲染器。
内核关系
react-reconciler接收输入,将fiber树生成逻辑封装到一个回调函数中- 把上述回调函数送入
scheduler接受调度 scheduler控制回调执行时机,执行完得到全新fiber树- 最后调用渲染器
react-dom,将fiber树形结构展示在界面上
工作循环区别
任务调度循环的数据结构是二叉堆,循环执行堆定点直到清空;fiber构造循环的数据结构是树,深度优先遍历执行任务调度循环的逻辑是宏观的,负责调度任务task,不关心具体实现;fiber构造循环的逻辑是具体实现,是任务task的一部分
主要对象
fiber对象
一个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 等选项). 【Effect副作用相关】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)
fiber与ReactElement对照关系
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,
|};
memoizedState: 内存状态, 用于输出成最终的fiber树baseState: 基础状态, 当Hook.queue更新过后,baseState也会更新.baseQueue: 基础状态队列, 在reconciler阶段会辅助状态合并.queue: 指向一个Update队列next: 指向该function组件的下一个Hook对象, 使得多个Hook之间也构成了一个链表.
优先级管理
分类
fiber优先级(LanePriority)
属于react-reconciler包,使用于与fiber构造过程相关的方法(fiber.updateQueue, fiber.lanes),定义于ReactFiberLane.js(见源码)
调度优先级(SchedulerPriority)
属于scheduler包,定义于SchedulerPriorities.js中(见源码)
优先级等级(ReactPriority)
属于react-reconciler包,定义于SchedulerWithReactIntegration.js中(见源码)
LanePriority与SchedulerPriority通过ReactPriorityLevel进行转换
调度原理
内核重要函数
shouldYieldToHost(是否让出主线程)
判定条件:
currentTime >= deadline: 只有时间超过deadline之后才会让出主线程(其中deadline = currentTime + yieldInterval).yieldInterval默认是5ms, 只能通过forceFrameRate函数来修改(事实上在 v17.0.2 源码中, 并没有使用到该函数).- 如果一个
task运行时间超过5ms, 下一个task执行之前, 会把控制权归还浏览器.
navigator.scheduling.isInputPending(): 这 facebook 官方贡献给 Chromium 的 api, 现在已经列入 W3C 标准(具体解释), 用于判断是否有输入事件(包括: input 框输入事件, 点击事件等)
任务队列管理
在scheduler中维护了一个任务队列taskQueue和延时任务队列timerQueue
创建任务
- 获取当前时间
var currentTime = getCurrentTime();
- 根据传入的优先级,设置任务过期时间
expirationTime
switch (priorityLevel) {
...
}
var expirationTime = startTime + timeout;
- 创建新任务
var newTask = {
id: taskIdCounter++, // id: 一个自增编号
callback, // callback: 传入的回调函数
priorityLevel, // priorityLevel: 优先级等级
startTime, // startTime: 创建task时的当前时间
expirationTime, // expirationTime: task的过期时间, 优先级越高 expirationTime = startTime + timeout 越小
sortIndex: -1,
};
- 加入任务队列
push(taskQueue, newTask);
- 请求调度
requestHostCallback(flushWork);
消费任务
创建任务的第5步请求调度传入调度中心内核的flushWork回调函数,其中会调用workLoop,其包含了队列消费的主要逻辑,也就是任务调度循环
每一次while循环的退出就是一个时间切片, 深入分析while循环的退出条件:
- 队列被完全清空: 这种情况就是很正常的情况, 一气呵成, 没有遇到任何阻碍.
- 执行超时: 在消费
taskQueue时, 在执行task.callback之前, 都会检测是否超时, 所以超时检测是以task为单位.- 如果某个
task.callback执行时间太长(如:fiber树很大, 或逻辑很重)也会造成超时 - 所以在执行
task.callback过程中, 也需要一种机制检测是否超时, 如果超时了就立刻暂停task.callback的执行.
- 如果某个
时间切片原理
消费任务队列的过程中,可以消费1-n个task。但每次执行task.callback前都要超时检测,如果超时可以立刻退出循环并等待下次调用
可中断渲染原理
在时间切片的基础之上,在fiber树构造过程中,没构造完成一个单元,都会做超时检测,如果单个task.callback执行时间很长就退出循环,并返回一个新的回调函数(continuationCallback)并等待下次回调继续未完成的fiber树构造
fiber树构造
基础准备
ReactElement对象 -> fiber对象 -> DOM对象
双缓冲技术
ReactElement转换为fiber树的过程中,内存中会同时存在2棵fiber树:
- 代表当前界面的树:已展示,挂载在
fiberRoot.current,初始化渲染时该fiber树为null - 正在构造的fiber树:即将展示,挂载在
HostRootFiber.alternate上,正在构造的节点成为workInProgress,构造完成后切换fiberRoot.current = workInProgress
优先级
update优先级
update对象中的lane属性代表其优先级,根据当前时间创建
两种情况下会创建update对象:
- 应用初始化:
react-reconciler包中updateContainer函数执行 - 发起组件更新:class组件中调用
setState
- 当在legacy或blocking模式中,如果执行上下文为空,会取消schedule调度,主动刷新回调,立即进入fiber树构造过程,当执行
setState下一行代码时,fiber树已经重新渲染了,则setState表现为同步- 正常情况下不会取消schedule调度,因为其由宏任务
MessageChannel触发,则setState表现为异步
渲染优先级
fiber优先级
fiber.lanes:本节点优先级fiber.childLanes:子节点优先级 初始值均为NoLanes,fiber树构造过程中,使用全局的渲染优先级renderLanes和fiber.lanes判断fiber节点是否更新- 如果
renderLanes不包括fiber.lanes,证明该fiber节点没有更新,可以复用 - 如果不能复用,则进入创建阶段
栈帧管理
全局变量记录了fiber树构造的活动,因此可通过这些变量还原其构造过程,如时间切片过程,全局变量用于在构造被打断之后还原进度。每次fiber树构造都是独立的过程,需要独立的一组全局变量,React内部将这个过程封装为一个栈帧
初次创建
在深度优先遍历构建中,每个fiber节点都会经历2个阶段
探寻阶段beginWork
- 根据
ReactElement对象创建所有fiber节点 - 设置二进制变量
fiber.flags,用于标记fiber节点增删改状态 - 设置
fiber.stateNode局部状态,如Class类型
回溯阶段completeWork
- 给fiber节点创建DOM实例,设置
fiber.stateNode局部状态 - 给DOM节点设置属性
【参考资料】
图解React 7kms.github.io/react-illus…