React 源码漂流记:React 调和器核心源码解读(一)
# 目录
# 前言
# updateContainer:星星之火,可以燎原
updateContainer 是燎原的第一颗火星。
先看一段代码:
// src/react/packages/react-reconciler/src/ReactFiberReconciler.new.js
export function updateContainer(
// 待挂载的组件
element: ReactNodeList,
// 挂载容器
container: OpaqueRoot,
): Lane {
// 获取 RootFiber
const current = container.current;
const eventTime = requestEventTime();
const lane = requestUpdateLane(current);
// 更新 container 的 context 信息
const context = getContextForSubtree();
// 创建一个更新
const update = createUpdate(eventTime, lane);
// 将新建的更新入栈
enqueueUpdate(current, update, lane);
// 请求一次调度更新
const root = scheduleUpdateOnFiber(current, lane, eventTime);
return lane;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果你调试过 React 的首次更新过程,你会知道 React 会走到这里。如果你往前追溯 updateContainer 的调用链条,你会发现这些调度都来自于应用层 API。首次渲染是一次同步渲染,通过 updateContainer 这个函数,创建第一个 update 对象,将首个 update 对象入队列,发起首次调度。可以说,这里是 React 引擎的点火器。
总结一下 updateContainer 核心功能:
- 初始化创建一个更新对象,并且将更新加入更新队列。
- 调用 scheduleUpdateOnFiber,(向调度器) 发出一次调度的请求。【注:向调度器有些不妥,因为同步任务一般不会经过调度器,这里暂且这么表述,便于理解】
# scheduleUpdateOnFiber:剥丝抽茧,追本溯源
scheduleUpdateOnFiber 从 FiberTree 的枝繁叶茂中找到了当初的那枚种子。
在 updateContainer 中,调用了 scheduleUpdateOnFiber 以在 fiber(此处指的是 RootFiber) 上调度一次更新,那么调度更新是如何在 fiber 上展开的呢?
首先来分析一下代码:
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
export function scheduleUpdateOnFiber(
// RootFiber
fiber: Fiber,
// 调度优先级
lane: Lane,
eventTime: number,
): FiberRoot | null {
// 检查嵌套更新,防止死循环
checkForNestedUpdates();
// 从 fiber 向上收集 lanes,root:FiberRoot = fiber.stateNode。对于 updateContainer 来说,这里 fiber 就是 RootFiber。
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
// 标记 root 即将更新,root.pendingLanes |= lane
markRootUpdated(root, lane, eventTime);
// 如果当前已经是 Render 阶段,且 root 是待处理的 FiberRoot,这时跳过渲染的调度请求,并且追踪 lane,加入到 Render 阶段的 lanes,就在在当前调度的回调中参与渲染,或者等待下次渲染。
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
// 收集当前的 lane 到 workInProgressRootRenderPhaseUpdatedLanes,表示在当前 render 中当前正在渲染的 RootFiber 上的优先级队列。
workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
workInProgressRootRenderPhaseUpdatedLanes,
lane,
);
} else {
// 确保 FiberRoot 发起调度请求
ensureRootIsScheduled(root, eventTime);
}
return root;
}
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
关注入参的伙伴可能已经发现,这里传入的是 fiber,返回的和传递给 ensureRootIsScheduled 函数的却是 root。root 是 FiberRoot,并不是 Fiber。可以把 FiberRoot 理解为 FiberTree 的容器。FiberRoot 与 RootFiber 双向索引。之后会详细展开。
markUpdateLaneFromFiberToRoot 向上收集优先级的同时寻找到了 FiberRoot 容器,因为渲染任务的调度是依托于容器的,而非 RootFiber。这个过程更像是一个抽丝剥茧的过程,root 才是被调度的目标。
这个函数的核心功能如下:
- 从 fiber 向父级收集 lanes,并且计算出 FiberRoot。
- 调用 ensureRootIsScheduled,确保 FiberRoot 发起同步或者异步调度。
# ensureRootIsScheduled:一花开两叶,结果自然成
ensureRootIsScheduled 是封装调度任务的双线流水车间。
在上面对 scheduleUpdateOnFiber 的分析中,最重要的就是调用 ensureRootIsScheduled,以保证在 fiber 所在的 FiberRoot 上调度更新,那么 FiberRoot 上是如何继续调度的呢?继续来看代码。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// 在本次调度之前的当前的记录的回调节点
const existingCallbackNode = root.callbackNode;
// 计算将要渲染的 lanes
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// 无需要渲染的 lanes,直接重置退出
if (nextLanes === NoLanes) {
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// 获取 lanes 中优先级最高的 lane 作为本次调度的优先级
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// 由于即将要生成新的调度,先将现在的调度节点上的回调取消掉
if (existingCallbackNode != null) {
cancelCallback(existingCallbackNode);
}
// 设置一个新的回调节点
let newCallbackNode;
// 如果是同步更新任务
if (newCallbackPriority === SyncLane) {
// 请求同步调度回调 performSyncWorkOnRoot,将该回调加入同步回调队列
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
if (supportsMicrotasks) {
// 支持微任务的浏览器不用再请求调度器的回调
scheduleMicrotask(() => {
if (executionContext === NoContext) {
// 消费完同步回调队列
flushSyncCallbacks();
}
});
} else {
// 不支持微任务则向调度器请求回调,优先级 ImmediatePriority(立即回调),回调后执行 flushSyncCallbacks 将同步回调队列消费完
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
// 同步更新执行完毕,将 newCallbackNode 置为 null,performSyncWorkOnRoot 不会用到此值
newCallbackNode = null;
} else {
let schedulerPriorityLevel;
// 将 lanes 转化为事件优先级,然后将事件优先级转化为调度优先级
switch (lanesToEventPriority(nextLanes)) {
// 离散事件优先级:ImmediateSchedulerPriority
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
// 连续事件优先级:UserBlockingSchedulerPriority
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
// 默认事件优先级:NormalSchedulerPriority
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
// Idle 事件优先级:IdleSchedulerPriority
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
// 向调度器请求相应优先级的异步回调【也可能是立即执行的优先级】,回调后执行 performConcurrentWorkOnRoot,Scheduler.scheduleCallback 返回调度的 callbackNode(newTask)
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// 更新 callbackPriority 和 callbackNode 注意,此时只是发起调度,回调并未执行
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
ensureRootIsScheduled 将 FiberRoot 上的调度任务区分为同步任务和异步任务。
这个函数有以下几个核心作用:
- 更新 root 的 callbackNode、callbackPriority 属性。下次 ensureRootIsScheduled 被调用会使用到。
- 同步更新调度:调用 scheduleSyncCallback 将同步回调 performSyncWorkOnRoot 推入同步回调队列 syncQueue;支持微任务的直接在微任务的回调执行 flushSyncCallbacks;不支持微任务时以 ImmediateSchedulerPriority 的优先级向调度器请求同步回调,回调时执行 flushSyncCallbacks 消费同步队列中所有的同步回调。
- 异步更新调度:根据 nextLanes 计算事件优先级,并且转化为调度优先级,以相应的调度优先级向调度器发起异步回调,回调时执行 performConcurrentWorkOnRoot。
- 注意同步调度中调用了 scheduleSyncCallback、scheduleCallback 两个函数不可混淆,scheduleCallback 是 Scheduler 提供的一种基于优先级机制的任务(回调)调度手段,performSyncWorkOnRoot 和 performConcurrentWorkOnRoot 才是真正要通过调度执行的任务。同步的任务通过同步回调队列的方式进行了优化处理。scheduleSyncCallback 是将同步的任务加入同步任务队列。调度器不是不可缺少的,如果浏览器支持微任务,同步任务的处理就可以交给微任务处理,而不经过调度器。
# 扩展
# 怎么理解 updateContainer 是 “引擎 “这件事?
我们可以从 updateContainer 的调用来源来展开下。
调用 updateContainer 的函数包括: legacyRenderSubtreeIntoContainer、ReactDOMRoot.prototype.render、ReactDOMRoot.prototype.unmount、hydrateRoot、scheduleRoot。ReactDOMRoot 是由 ReactDOM.createRoot 创建的。
// 应用层 API
legacyRenderSubtreeIntoContainer <- ReactDOM.hydrate
<- ReactDOM.render
<- ReactDOM.unmountComponentAtNode
2
3
4
由上面对函数调用链的分析可以看出,updateContainer 主要来源于应用层 API 的调用,加上 updateContainer 跟 scheduleUpdateOnFiber 的关系,可以看出 updateContainer 确实是针对 container 这个容器上的调度更新的入口而存在的,而这个 container,就是 FiberRoot。
# scheduleMicrotask 与 queueMicrotask
上文我们已经了解到支持微任务的浏览器会使用微任务的形式消费完(flush)同步任务队列,那么这个微任务是什么呢?下面来展开一下 scheduleMicrotask 的代码:
const localPromise = typeof Promise === 'function' ? Promise : undefined;
export const scheduleTimeout: any =
typeof setTimeout === 'function' ? setTimeout : (undefined: any);
export const scheduleMicrotask: any =
typeof queueMicrotask === 'function'
? queueMicrotask
: typeof localPromise !== 'undefined'
? callback =>
localPromise
.resolve(null)
.then(callback)
.catch(handleErrorInNextTick)
: scheduleTimeout;
function handleErrorInNextTick(error) {
// 非阻塞式抛出异常
setTimeout(() => {
throw error;
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
什么是 queueMicrotask?
queueMicrotask adds the function (task) into a queue and each function is executed one by one (FIFO) after the current task has completed its work and when there is no other code waiting to be run before control of the execution context is returned to the browser's event loop.【来自 MDN】
微任务使用 queueMicrotask,同时 queueMicrotask 可以由 Promise 来模拟,或者使用 setTimeout 优雅替代。
# 问题
# 位运算怎么理解?
关于 react 中常见的位运算,在之后的文章中会单独详解。本文主要用到 |
运算,按位或运算的规则是:两个位都为 0 时,结果才为 0。在这里举出一个例子,方便大家对文章的 |=
进行理解:
const NoContext = 0b0000;
const BatchedContext = 0b0001;
const RenderContext = 0b0010;
let executionContext = NoContext;
// 如果现在开始 RenderContainer,进入 Batch 阶段
// 增加枚举值
executionContext |= BatchedContext; // 1
// 判断是否在 Batch 阶段
// 消费枚举值:0 表示没有枚举值,1 表示有枚举值。这里我们直接跟为 0 的 NoContext 作比较。
(executionContext & BatchedContext) !== NoContext; // true
// 判断是否处于 Render 阶段
(executionContext & RenderContext) !== NoContext; // false
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# lanes 优先级怎么理解?
React 中需要基于优先级的调度机制以区分不同渲染任务的轻重缓急,在 v16 版本的 React 中还是使用 expirationTime 来管理优先级,在 v17 的版本中则采用了 lane 模型,相比于 expirationTime 模型,lane 模型有着更为细粒度、效率更高的特性。关于调度与优先级的内容,之后的文章会详述。
# FiberRoot 和 RootFiber 什么关系?
在本文中反复提到了 FiberRoot 和 RootFiber(HostRoot),关于两者的区别如下:
- FiberRoot 和 RootFiber 具有双向链接关系。FiberRoot.current = RootFiber,RootFiber.stateNode = FiberRoot。
- FiberRoot 是 FiberTree 的容器,记录 FiberTree 在渲染过程中的数据。
- RootFiber 本质上是 Fiber,是 FiberTree 的根节点,是特殊的 Fiber。
- HostRoot 也就是 HostRootFiber,RootFiber 被标记为 HostRoot。
在之后的文章中会详述 Fiber、RootFiber 和 FiberRoot、以及 FiberTree 的结构。
# 总结
总结一下本文的内容:
- updateContainer:初始化更新任务,调用 scheduleUpdateOnFiber 发出调度请求。
- scheduleUpdateOnFiber:收集优先级,计算 FiberRoot。调用 ensureRootIsScheduled,确保 FiberRoot 发起同步或者异步调度。
- ensureRootIsScheduled:包装同步更新任务和异步更新任务并采用不同的调度策略。同步更新任务入同步任务队列在微任务中执行,异步更新任务交给调度器进行调度与回调。