React运行原理

宏观包结构

基础包结构

  1. react

    react 基础包, 只提供定义 react 组件(ReactElement)的必要函数, 一般来说需要和渲染器(react-dom,react-native)一同使用. 在编写react应用的代码时, 大部分都是调用此包的 api.

  2. react-dom

    react 渲染器之一, 是 react 与 web 平台连接的桥梁(可以在浏览器和 nodejs 环境中使用), react-reconciler中的运行结果输出到 web 界面上. 在编写react应用的代码时,大多数场景下, 能用到此包的就是一个入口函数ReactDOM.render(<App/>, document.getElementById('root')), 其余使用的 api, 基本是react包提供的.

  3. react-reconciler

    react 得以运行的核心包(综合协调react-dom,react,scheduler各包之间的调用与配合).
    管理 react 应用状态的输入和结果的输出. 将输入信号最终转换成输出信号传递给渲染器.

    • 接受输入(scheduleUpdateOnFiber), 将fiber树生成逻辑封装到一个回调函数中(涉及fiber树形结构, fiber.updateQueue队列, 调和算法等),
    • 把此回调函数(performSyncWorkOnRootperformConcurrentWorkOnRoot)送入scheduler进行调度
    • scheduler会控制回调函数执行的时机, 回调函数执行完成后得到全新的 fiber 树
    • 再调用渲染器(如react-dom, react-native等)将 fiber 树形结构最终反映到界面上
  4. scheduler

    调度机制的核心实现, 控制由react-reconciler送入的回调函数的执行时机, 在concurrent模式下可以实现任务分片. 在编写react应用的代码时, 同样几乎不会直接用到此包提供的 api.

    • 核心任务就是执行回调(回调函数由react-reconciler提供)
    • 通过控制回调函数的执行时机, 来达到任务分片的目的, 实现可中断渲染(concurrent模式下才有此特性)

宏观总览

为了便于理解, 可将 react 应用整体结构分为接口层(api)和内核层(core)2 个部分

  1. 接口层(api)

    react包, 平时在开发过程中使用的绝大部分api均来自此包(不是所有). 在react启动之后, 正常可以改变渲染的基本操作有 3 个.

    • class 组件中使用setState()
    • function 组件里面使用 hook,并发起dispatchAction去改变 hook 对象
    • 改变 context(其实也需要setStatedispatchAction的辅助才能改变)

    以上setStatedispatchAction都由react包直接暴露. 所以要想 react 工作, 基本上是调用react包的 api 去与其他包进行交互.

  2. 内核层(core)

    整个内核部分, 由 3 部分构成:

    1. 调度器(scheduler)

      核心职责只有 1 个, 就是执行回调.

      • react-reconciler提供的回调函数, 包装到一个任务对象中.
      • 在内部维护一个任务队列, 优先级高的排在最前面.
      • 循环消费任务队列, 直到队列清空.
    2. 构造器(react-reconciler)

      有 3 个核心职责:

      1. 装载渲染器, 渲染器必须实现HostConfig协议(如: react-dom), 保证在需要的时候, 能够正确调用渲染器的 api, 生成实际节点(如: dom节点).
      2. 接收react-dom包(初次render)和react包(后续更新setState)发起的更新请求.
      3. fiber树的构造过程包装在一个回调函数中, 并将此回调函数传入到scheduler包等待调度.
    3. 渲染器(react-dom)

      有 2 个核心职责:

      1. 引导react应用的启动(通过ReactDOM.render).
      2. 实现HostConfig协议(源码在 ReactDOMHostConfig.js 中), 能够将react-reconciler包构造出来的fiber树表现出来, 生成 dom 节点(浏览器中), 生成字符串(ssr).

内核关系

总结

react主要有四个包,react、react-dom(渲染器)、react-reconciler(构造器)、scheduler(调度器)。

react提供了一些我们平时编写代码时常用的API。

react-dom 将 react-reconciler 包构造出来的 fiber 树表现出来, 生成 dom 节点。当然还负责通过 ReactDOM.render 来启动 React 应用。

react-reconciler 主要用于接受更新的请求(请求可能来源于react-dom的初次render,也可能来源于react的setState),将 fiber 树的构造或更新过程包装为一个回调,然后将这个回调传递给 scheduler 调度。

schduler 接收到 reconciler 传过来的回调,将其包装为一个 task。内部会维护一个任务队列,优先级高的排在最前面,循环消费任务队列,直到任务队列为空。(schduler 是可以脱离 react 的,从包的名字上也可以看出来,本质上它就是一个根据优先级消费任务的算法)

工作循环与主干逻辑

任务调度循环(调度任务)

位于 scheduler 中 ,它是react应用得以运行的保证,它需要循环调用,控制所有任务(task)的调度。

优先级任务队列以二叉堆为数据结构,循环执行堆,直到堆被清空。

  • 二叉堆插入和删除高效稳定O(logn)

调度每一个任务,但不关系这个任务干什么,只需要执行回调函数 performSyncWorkOnRootperformConcurrentWorkOnRoot

fiber 构造循环(任务的一部分)

位于 react-reconciler 中,控制 fiber 树的构造。

fiber 构造循环是以树为数据结构,从上至下执行深度优先遍历。

fiber 构造循环是任务的一部分,只负责 fiber 树的构造。实际上一个任务可能包括fiber树的构造,DOM渲染,调度检测。

主干逻辑

1.将每一次更新(如DOM变更)视为一次更新需求

2.react-reconciler 收到更新需求后,会去 scheduler 将更新需求转化一个任务

3.scheduler 通过任务调度循环来执行 task

4.task的实际执行过程发生在 react-reconciler 中

  • fiber构造循环,循环完成后构造出最新的 fiber 树
  • commitRoot,把最新的 fiber 树最终渲染到页面上,任务完成

主干逻辑就是从DOM更新到实际渲染这一条链路, 为了更好的性能(如批量更新, 可中断渲染等功能),react在输入到输出的链路上做了很多优化策略,比如本文讲述的任务调度循环和fiber构造循环相互配合就可以实现可中断渲染

==高频对象==

react包

ReactElement

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

包括<App/>及其所有子节点都是ReactElement对象,每个ReactElement对象的区别在于 type 不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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,
};
  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类型,则会调用该方法获取子节点
ReactComponent(class类型组件)

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

对于开发者来讲,ReactComponent使用非常高频(在状态组件章节中详细解读)。

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

function类型的组件和class类型的组件一样, 是诸多ReactElement形式中的一种.

react-reconciler包

Fiber对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 一个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树和ReactElement结构:

image-20240227141232333
  • 其中<App/>,<Content/>ClassComponent类型的fiber节点, 其余节点都是普通HostComponent类型节点.
  • <Content/>的子节点在ReactElement树中是React.Fragment, 但是在fiber树中React.Fragment并没有与之对应的fiber节点(reconciler阶段对此类型节点做了单独处理, 所以ReactElement节点和fiber节点不是一对一匹配).

reconciler运作流程

react-reconciler的主要作用

  1. 输入: 暴露api函数(如: scheduleUpdateOnFiber), 供给其他包(如react包)调用。
  2. 注册调度任务: 与调度中心(scheduler包)交互, 注册调度任务task, 等待任务回调。
  3. 执行任务回调: 在内存中构造出fiber树, 同时与与渲染器(react-dom)交互, 在内存中创建出与fiber对应的DOM节点。
  4. 输出: 与渲染器(react-dom)交互, 渲染DOM节点。

输入

无论是首次渲染还是后续更新,最后都会间接调用scheduleUpdateOnFiber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 唯一接收输入信号的函数
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// ... 省略部分无关代码
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (lane === SyncLane) {
if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
// 直接进行`fiber构造`
performSyncWorkOnRoot(root);
} else {
// 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`
ensureRootIsScheduled(root, eventTime);
}
} else {
// 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`
ensureRootIsScheduled(root, eventTime);
}
}

逻辑进入到scheduleUpdateOnFiber之后, 后面有 2 种可能:

  1. 不经过调度, 直接进行fiber构造.
  2. 注册调度任务, 经过Scheduler包的调度, 间接进行fiber构造.

注册调度任务

与输入环节紧密相连, scheduleUpdateOnFiber函数之后, 立即进入ensureRootIsScheduled函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ... 省略部分无关代码
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// 前半部分: 判断是否需要注册新的调度
const existingCallbackNode = root.callbackNode;
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
const newCallbackPriority = returnNextLanesPriority();
if (nextLanes === NoLanes) {
return;
}
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
return;
}
cancelCallback(existingCallbackNode);
}

// 后半部分: 注册调度任务
let newCallbackNode;
if (newCallbackPriority === SyncLanePriority) {
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
} else if (newCallbackPriority === SyncBatchedLanePriority) {
newCallbackNode = scheduleCallback(
ImmediateSchedulerPriority,
performSyncWorkOnRoot.bind(null, root),
);
} else {
const schedulerPriorityLevel =
lanePriorityToSchedulerPriority(newCallbackPriority);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}

ensureRootIsScheduled的逻辑很清晰, 分为 2 部分:

  1. 前半部分: 判断是否需要注册新的调度(如果无需新的调度, 会退出函数)
  2. 后半部分: 注册调度任务
    • performSyncWorkOnRootperformConcurrentWorkOnRoot被封装到了任务回调(scheduleCallback)中
    • 等待调度中心执行任务, 任务运行其实就是执行performSyncWorkOnRootperformConcurrentWorkOnRoot

执行任务回调

实际上就是执行performSyncWorkOnRootperformConcurrentWorkOnRoot

performSyncWorkOnRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ... 省略部分无关代码
function performSyncWorkOnRoot(root) {
let lanes;
let exitStatus;

lanes = getNextLanes(root, NoLanes);
// 1. fiber树构造
exitStatus = renderRootSync(root, lanes);

// 2. 异常处理: 有可能fiber构造过程中出现异常
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
// ...
}

// 3. 输出: 渲染fiber树
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
commitRoot(root);

// 退出前再次检测, 是否还有其他更新, 是否需要发起新调度
ensureRootIsScheduled(root, now());
return null;
}

performSyncWorkOnRoot的逻辑很清晰, 分为 3 部分:

  1. fiber 树构造
  2. 异常处理: 有可能 fiber 构造过程中出现异常
  3. 调用输出

performConcurrentWorkOnRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// ... 省略部分无关代码
function performConcurrentWorkOnRoot(root) {

const originalCallbackNode = root.callbackNode;

// 1. 刷新pending状态的effects, 有可能某些effect会取消本次任务
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects) {
if (root.callbackNode !== originalCallbackNode) {
// 任务被取消, 退出调用
return null;
} else {
// Current task was not canceled. Continue.
}
}
// 2. 获取本次渲染的优先级
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// 3. 构造fiber树
let exitStatus = renderRootConcurrent(root, lanes);

if (
includesSomeLane(
workInProgressRootIncludedLanes,
workInProgressRootUpdatedLanes,
)
) {
// 如果在render过程中产生了新的update, 且新update的优先级与最初render的优先级有交集
// 那么最初render无效, 丢弃最初render的结果, 等待下一次调度
prepareFreshStack(root, NoLanes);
} else if (exitStatus !== RootIncomplete) {
// 4. 异常处理: 有可能fiber构造过程中出现异常
if (exitStatus === RootErrored) {
// ...
}.
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// 5. 输出: 渲染fiber树
finishConcurrentRender(root, exitStatus, lanes);
}

// 退出前再次检测, 是否还有其他更新, 是否需要发起新调度
ensureRootIsScheduled(root, now());
if (root.callbackNode === originalCallbackNode) {
// 渲染被阻断, 返回一个新的performConcurrentWorkOnRoot函数, 等待下一次调用
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}

performConcurrentWorkOnRoot的逻辑与performSyncWorkOnRoot的不同之处在于, 对于可中断渲染的支持

  1. 调用performConcurrentWorkOnRoot函数时, 首先检查是否处于render过程中, 是否需要恢复上一次渲染。
  2. 如果本次渲染被中断, 最后返回一个新的 performConcurrentWorkOnRoot 函数, 等待下一次调用。

commit提交

在输出阶段,commitRoot的实现逻辑是在commitRootImpl函数中, 其主要逻辑是处理副作用队列, 将最新的 fiber 树结构反映到 DOM 上.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ... 省略部分无关代码
function commitRootImpl(root, renderPriorityLevel) {
// 设置局部变量
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;

// 清空FiberRoot对象上的属性
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;

// 提交阶段
let firstEffect = finishedWork.firstEffect;
if (firstEffect !== null) {
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 阶段1: dom突变之前
nextEffect = firstEffect;
do {
commitBeforeMutationEffects();
} while (nextEffect !== null);

// 阶段2: dom突变, 界面发生改变
nextEffect = firstEffect;
do {
commitMutationEffects(root, renderPriorityLevel);
} while (nextEffect !== null);
root.current = finishedWork;

// 阶段3: layout阶段, 调用生命周期componentDidUpdate和回调函数等
nextEffect = firstEffect;
do {
commitLayoutEffects(root, lanes);
} while (nextEffect !== null);
nextEffect = null;
executionContext = prevExecutionContext;
}
ensureRootIsScheduled(root, now());
return null;
}

核心逻辑分为 3 个步骤:

  1. commitBeforeMutationEffects

dom 变更之前, 主要处理副作用队列中带有Snapshot,Passive标记的fiber节点.

  1. commitMutationEffects

dom 变更, 界面得到更新. 主要处理副作用队列中带有Placement, Update, Deletion, Hydrating标记的fiber节点.

  1. commitLayoutEffects

dom 变更后, 主要处理副作用队列中带有Update | Callback标记的fiber节点.

==总结==

无论是首次渲染还是后续更新,都会进入一个函数 scheduleUpdateOnFiber,这个函数是 react-reconciler 暴露给其它包的,这个函数主要是根据上下文来直接或间接的执行 fiber 构造。所谓间接就是要注册调度任务,然后经过 Scheduler 的调度。

注册调度任务就是将 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 封装为回调,放到任务队列,等待执行,执行任务也就是执行这两个函数,当这两个函数被执行时,就进入 render 阶段,会以dfs的方式对比老的 Fiber 节点和新的组件返回的 JSX 对象,生成新的 Fiber 节点以及 effectList。

这两个函数的区别就在于对于可中断渲染的支持,performConcurrentWorkOnRoot 支持可中断渲染,因此会在每次调用时检查是否处于 render 的过程中,如果是就恢复上一次渲染。

然后进入 commit 阶段,基于 effectList 生成新的DOM。

优先级管理

可中断渲染,时间切片(time slicing),异步渲染(suspense) 等特性, 在源码中得以实现都依赖于优先级管理。

React内部对于优先级的管理, 贯穿运作流程的 4 个阶段(从输入到输出), 根据其功能的不同, 可以分为 3 种类型:

  1. fiber优先级(LanePriority): 位于react-reconciler包, 也就是Lane(车道模型).
  2. 调度优先级(SchedulerPriority): 位于scheduler包.
  3. 优先级等级(ReactPriorityLevel) : 位于react-reconciler包中的SchedulerWithReactIntegration.js, 负责上述 2 套优先级体系的转换.

Lane

Lane是什么

使用31位的二进制表示31条赛道,位数越小的赛道优先级越高,某些相邻的赛道拥有相同优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010;

export const InputDiscreteHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /* */ 0b0000000000000000000000000011000;

const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000000011000000;

export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000;
export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111000000000;

const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;

const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;

export const SomeRetryLane: Lanes = /* */ 0b0000010000000000000000000000000;

export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;

const NonIdleLanes = /* */ 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
const IdleLanes: Lanes = /* */ 0b0110000000000000000000000000000;

export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;

Lanes

可以看到其中有几个变量占用了几条赛道,比如:

1
2
3
const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;
export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111000000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;

这就是的概念,被称作lanes(区别于优先级lane)。

其中InputDiscreteLanes是“用户交互”触发更新会拥有的优先级范围。

DefaultLanes是“请求数据返回后触发更新”拥有的优先级范围。

TransitionLanesSuspenseuseTransitionuseDeferredValue拥有的优先级范围。

这其中有个细节,越低优先级lanes占用的位越多。比如InputDiscreteLanes占了2个位,TransitionLanes占了9个位。

原因在于:越低优先级更新越容易被打断,导致积压下来,所以需要更多的位。相反,最高优的同步更新的SyncLane不需要多余的lanes

为什么这么设计

主要是方便我们使用位操作来实现一些比较、合并、重置等功能。

Lane有关操作

生成 lane

我们可以使用左移操作来快速生成一个对应的 lane,比如我们想要生成一个 DefaultLane 我们只需要用下面的代码即可

1
const lane = 1 << 5;
筛选 lanes

我们有时候需要判断哪些 lane 已经有任务了,哪些 lane 没有任务,这个时候我们需要使用我们的 lanes 和我们预定义的一些 lanes 内容进行按位与的操作,得到的内容就是我们哪些对应的车道上有任务,比如我们现在想要知道在非闲置车道上哪些车道已经有任务了,我们只需要用 NonIdleLanes 和我们 lanes 做按位与操作即可

1
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;

如果我们想要筛选出不包含某些车道的任务,我们只需要先把筛选模型按位取反再按位与即可:

1
const IdlePendingLanes = pendingLanes & ~NonIdleLanes;
合并 lanes

如果想把 lane 合并到 lanes 中,我们只需要进行按位或的操作就行了

1
root.pendingLanes |= updateLan
取出优先级最高的已经使用的 lane

取出优先级最高的任务,也就是二进制中最右边的那个 1。

操作是 lanes 与自己的负数进行按位与操作,因为我们的负数是使用补码保存的,如果最后一位是 1 ,那么补码就会变成 1,与操作就会返回 1;如果最后一位是 0 ,补码就是 0,但是会产生一位的进位,这个进位会持续到遇到 0 为止,而这个 0 就是 1的补码,此时就会产生 1 了,与操作就会返回 1。

1
lanes & -lanes;
删除对应的 lane

我们有时候会从我们总的 lanes 里取出我们需要 lane 的然后重置它,我们可以对指定的车道的值按位取反,然后和我们的 lanes 进行按位与操作,这时除了我们需要的那个车道是 0 其他都是 1 ,按位与的时候,我们重置的车道必定变成 0,而其他的车道会变成保留自己原来的值:

1
lanes &= ~lane;

LanePriority

LanePriority: 属于react-reconciler包, 定义于 ReactFiberLane.js

1
2
3
4
5
6
7
8
9
10
export const SyncLanePriority: LanePriority = 15;
export const SyncBatchedLanePriority: LanePriority = 14;

const InputDiscreteHydrationLanePriority: LanePriority = 13;
export const InputDiscreteLanePriority: LanePriority = 12;

// .....

const OffscreenLanePriority: LanePriority = 1;
export const NoLanePriority: LanePriority = 0;

fiber构造过程相关的优先级(如fiber.updateQueue,fiber.lanes)都使用LanePriority

SchedulerPriority

SchedulerPriority, 属于scheduler包, 定义于SchedulerPriorities.js

1
2
3
4
5
6
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

scheduler调度中心相关的优先级使用SchedulerPriority.

ReactPriorityLevel

reactPriorityLevel, 属于react-reconciler包, 定义于SchedulerWithReactIntegration.js

用于转换 SchedulerPriority 和 LanePriority,协同调度中心(scheduler包)和 fiber 树构造(react-reconciler包)中对优先级的使用。

1
2
3
4
5
6
7
export const ImmediatePriority: ReactPriorityLevel = 99;
export const UserBlockingPriority: ReactPriorityLevel = 98;
export const NormalPriority: ReactPriorityLevel = 97;
export const LowPriority: ReactPriorityLevel = 96;
export const IdlePriority: ReactPriorityLevel = 95;
// NoPriority is the absence of priority. Also React-only.
export const NoPriority: ReactPriorityLevel = 90;

参考

https://blog.csdn.net/weixin_46463785/article/details/130402847

调度原理(Scheduler)

原理图

调度相关核心函数

1
2
3
4
5
6
7
8
export let requestHostCallback; // 请求及时回调: port.postMessage
export let cancelHostCallback; // 取消及时回调: scheduledHostCallback = null
export let requestHostTimeout; // 请求延时回调: setTimeout
export let cancelHostTimeout; // 取消延时回调: cancelTimeout
export let shouldYieldToHost; // 是否让出主线程(currentTime >= deadline && needsPaint): 让浏览器能够执行更高优先级的任务(如ui绘制, 用户输入等)
export let requestPaint; // 请求绘制: 设置 needsPaint = true
export let getCurrentTime; // 获取当前时间
export let forceFrameRate; // 强制设置 yieldInterval (让出主线程的周期). 这个函数虽然存在, 但是从源码来看, 几乎没有用到

调度

请求或取消调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 接收 MessageChannel 消息
const performWorkUntilDeadline = () => {
// ...省略无关代码
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// 更新deadline
deadline = currentTime + yieldInterval;
// 执行callback
scheduledHostCallback(hasTimeRemaining, currentTime);
} else {
isMessageLoopRunning = false;
}
};

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;

// 请求回调
requestHostCallback = function (callback) {
// 1. 保存callback
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
// 2. 通过 MessageChannel 发送消息
port.postMessage(null);
}
};
// 取消回调
cancelHostCallback = function () {
scheduledHostCallback = null;
};

请求回调之后scheduledHostCallback = callback, 然后通过MessageChannel发消息的方式触发performWorkUntilDeadline函数, 最后执行回调scheduledHostCallback.

此处需要注意: MessageChannel在浏览器事件循环中属于宏任务, 所以调度中心永远是异步执行回调函数.

关于MessageChannel

https://juejin.cn/post/6889314677528985614?searchId=202402272149034FDB259ADEE976088AD9

时间切片

执行时间分割, 让出主线程(把控制权归还浏览器, 浏览器可以处理用户输入, UI 绘制等紧急任务).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const localPerformance = performance;
// 获取当前时间
getCurrentTime = () => localPerformance.now();

// 时间切片周期, 默认是5ms(如果一个task运行超过该周期, 下一个task执行之前, 会把控制权归还浏览器)
let yieldInterval = 5;

let deadline = 0;
const maxYieldInterval = 300;
let needsPaint = false;
const scheduling = navigator.scheduling;
// 是否让出主线程
shouldYieldToHost = function () {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
if (needsPaint || scheduling.isInputPending()) {
// There is either a pending paint or a pending input.
return true;
}
// There's no pending input. Only yield if we've reached the max
// yield interval.
return currentTime >= maxYieldInterval; // 在持续运行的react应用中, currentTime肯定大于300ms, 这个判断只在初始化过程中才有可能返回false
} else {
// There's still time left in the frame.
return false;
}
};

// 请求绘制
requestPaint = function () {
needsPaint = true;
};

// 设置时间切片的周期
forceFrameRate = function (fps) {
if (fps < 0 || fps > 125) {
// Using console['error'] to evade Babel and ESLint
console['error'](
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing frame rates higher than 125 fps is not supported',
);
return;
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
// reset the framerate
yieldInterval = 5;
}
};

完整回调的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime(); // 1. 获取当前时间
deadline = currentTime + yieldInterval; // 2. 设置deadline
const hasTimeRemaining = true;
try {
// 3. 执行回调, 返回是否有还有剩余任务
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
if (!hasMoreWork) {
// 没有剩余任务, 退出
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
port.postMessage(null); // 有剩余任务, 发起新的调度
}
} catch (error) {
port.postMessage(null); // 如有异常, 重新发起调度
throw error;
}
} else {
isMessageLoopRunning = false;
}
needsPaint = false; // 重置开关
};

任务队列管理

调度的目的是为了消费任务, 接下来就具体分析任务队列是如何管理与实现的。

Scheduler.js中, 维护了一个taskQueue, 任务队列管理就是围绕这个taskQueue展开.

1
2
3
// Tasks are stored on a min heap
var taskQueue = [];
var timerQueue = [];

创建任务

unstable_scheduleCallback函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 省略部分无关代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 1. 获取当前时间
var currentTime = getCurrentTime();
var startTime;
if (typeof options === 'object' && options !== null) {
// 从函数调用关系来看, 在v17.0.2中,所有调用 unstable_scheduleCallback 都未传入options
// 所以省略延时任务相关的代码
} else {
startTime = currentTime;
}
// 2. 根据传入的优先级, 设置任务的过期时间 expirationTime
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
var expirationTime = startTime + timeout;
// 3. 创建新任务
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (startTime > currentTime) {
// 省略无关代码 v17.0.2中不会使用
} else {
newTask.sortIndex = expirationTime;
// 4. 加入任务队列
push(taskQueue, newTask);
// 5. 请求调度
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}

任务对象

1
2
3
4
5
6
7
8
9
var newTask = {
id: taskIdCounter++, // id: 一个自增编号
callback, // callback: 传入的回调函数
priorityLevel, // priorityLevel: 优先级等级
startTime, // startTime: 创建task时的当前时间
expirationTime, // expirationTime: task的过期时间, 优先级越高 expirationTime = startTime + timeout 越小
sortIndex: -1,
};
newTask.sortIndex = expirationTime; // sortIndex: 排序索引, 全等于过期时间. 保证过期时间越小, 越紧急的任务排在最前面

消费任务

创建任务之后, 最后请求调度requestHostCallback(flushWork)flushWork函数作为参数被传入调度中心内核等待回调。

在调度中心中, 只需下一个事件循环就会执行回调, 最终执行flushWork。flushWork中会执行 workLoop。

队列消费的主要逻辑是在workLoop函数中,这部分就是任务调度循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 省略部分无关代码
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime; // 保存当前时间, 用于判断任务是否过期
currentTask = peek(taskQueue); // 获取队列中的第一个任务
while (currentTask !== null) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// 虽然currentTask没有过期, 但是执行时间超过了限制(毕竟只有5ms, shouldYieldToHost()返回true). 停止继续执行, 让出主线程
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 执行回调
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
// 回调完成, 判断是否还有连续(派生)回调
if (typeof continuationCallback === 'function') {
// 产生了连续回调(如fiber树太大, 出现了中断渲染), 保留currentTask
currentTask.callback = continuationCallback;
} else {
// 把currentTask移出队列
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
} else {
// 如果任务被取消(这时currentTask.callback = null), 将其移出队列
pop(taskQueue);
}
// 更新currentTask
currentTask = peek(taskQueue);
}
if (currentTask !== null) {
return true; // 如果task队列没有清空, 返回true. 等待调度中心下一次回调
} else {
return false; // task队列已经清空, 返回false.
}
}

时间切片(time slicing) 和 fiber树的可中断渲染。 这 2 大特性的实现, 都集中于这个while循环。

每一次while循环的退出就是一个时间切片, 深入分析while循环的退出条件:

  1. 队列被完全清空: 这种情况就是很正常的情况, 一气呵成, 没有遇到任何阻碍.
  2. 执行超时: 在消费 taskQueue 时, 在执行 task.callback 之前, 都会检测是否超时, 所以超时检测是以 task 为单位.
    • 如果某个task.callback执行时间太长(如: fiber树很大, 或逻辑很重)也会造成超时
    • 所以在执行task.callback过程中, 也需要一种机制检测是否超时, 如果超时了就立刻暂停task.callback的执行.

时间切片原理

消费任务队列的过程中, 可以消费1~n个 task, 甚至清空整个 queue. 但是在每一次具体执行task.callback之前都要进行超时检测, 如果超时可以立即退出循环并等待下一次调用.

可中断渲染原理

在时间切片的基础之上, 如果单个task.callback执行时间就很长(假设 200ms). 就需要task.callback自己能够检测是否超时, 所以在 fiber 树构造过程中, 每构造完成一个单元, 都会检测一次超时(源码链接), 如遇超时就退出fiber树构造循环, 并返回一个新的回调函数(就是此处的continuationCallback)并等待下一次回调继续未完成的fiber树构造.

总结

scheduler 会维护一个基于大顶堆的任务队列,接受回调并包装成 task 对象(可能是组件的初次渲染、更新或提交),根据优先级被添加到任务队列中,schduler 有自己的优先级 schdulerPriority。为了避免任务过长而导致页面卡顿,就采用了时间分片的思想,将任务拆分成多个时间片段,初始是5ms,如果任务超时了,就中断并将控制权还给浏览器,去做layout、paint等事情,这样就基本能保证不会掉帧卡顿了。然后在下一帧再恢复到之前中断的地方继续往下执行。

scheduler 经常需要执行 reconciler 传递过来的构建 fiber 的任务。fiber 的引入正是为了支持可中断性和恢复,如果使用原先的递归比较虚拟 DOM 的方法来做 diff,那是没有办法在中端之后恢复原位的。Fiber架构采用了深度优先遍历的方式,允许在遍历树的过程中暂停和恢复,每个Fiber节点都包含了大量上下文的信息,因此可以非常方便的复位。

Fiber是怎么实现中断和恢复的?

在每处理完成一个 Fiber 节点时,会检查时间片是否到时,如果到了,则会中断此次 “渲染”

注:Reconciliation 阶段React Fiber会找出需要更新哪些DOM, 是可以被打断的,但是到了第二阶段Commit Phase ,那就一鼓作气把DOM更新完,绝不会被打断。


React运行原理
http://example.com/2024/02/27/React运行原理/
Author
John Doe
Posted on
February 27, 2024
Licensed under