React 源码漂流记:React 调和器核心源码解读(三)
# 目录
# 前言
在上一篇文章中,我们探讨了 React 调和器中 scheduleSyncCallback
、 scheduleCallback
、 performSyncWorkOnRoot
和 performConcurrentWorkOnRoot
四个核心函数,概括而言,其作用是:同步任务和异步任务的调度和任务回调。在本篇文章中我们就沿着上文中任务回调的入口继续深入,探讨调和器中 Render
的过程。
# renderRootSync
这个函数是在同步任务的回调的 Render 阶段调用,目的是对当前的 FiberRoot 进行渲染。
注意
这里所谓的渲染,并不是浏览器的渲染,即将 VDOM 转化为 DOM 并绘制到浏览器的过程。需要注意的是,此处 Render
过程,指的是 React 内部的 ExecutionContext 中的 RenderContext
, 即渲染过程实际上是一棵 FiberTree 真正调和的过程。所谓调和,就是新的 FiberTree 替代旧的 FiberTree,成为 currentFiberTree 的过程(FiberTree 的双缓存结构,后文详述)。而真正将 VDOM 转化为 DOM(或者 Render String),则是在 Commit
过程之后完成的。
下面我们来看下源码:
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function renderRootSync(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
// executionContext 添加 RenderContext
executionContext |= RenderContext;
// 更改当前 dispatcher 为 contextOnlyDispatcher,并且返回原来的 dispatcher
const prevDispatcher = pushDispatcher();
// ......
do {
try {
// 启动 workLoop
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// ......
// Render 阶段结束,恢复之后的 dispatcher
executionContext = prevExecutionContext;
popDispatcher(prevDispatcher);
// ......
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
return workInProgressRootExitStatus;
}
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
这里有几点核心的内容:
workLoopSync
开启了一个渲染循环,这样一个循环就体现在遍历的思想上,是对 FiberTree 进行一个深度优先遍历(DFS)。我们将在后文进行详细的探讨。- 返回
workInProgressRootExitStatus
是在整个src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
文件中维护的,也就是在整个ReactFiberWorkLoop
中维护的。在 workLoop 的执行过程中,exitStatus
总能保持最新的执行状态。 - 在执行到
renderRootSync
函数中时,executionContext
被更新到RenderContext
状态。这里有位运算的内容,详细可参见位运算怎么理解?。
另外,还有一些值得注意的问题:
pushDispatcher
和popDispatcher
是在做什么?dispatcher 实际上是和 hook 相关的内容,在pushDispatcher
中将ReactCurrentDispatcher.current
设置为ContextOnlyDispatcher
,这种状态下的 hook 在调用时会报错。这是因为 hook 在Render
阶段是不可调用的。我们在 hook 原理相关的章节会详细介绍。- 在执行完
Render
过程之后,executionContext
恢复了之前的状态,即Batch
状态。这也是为什么在进入Render
状态和Commit
状态之前都要检查是否不是已经处于这两种状态。
# renderRootConcurrent
这个函数是在异步任务的回调的 Render
阶段调用,目的是对当前的 FiberRoot 进行渲染。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
const prevExecutionContext = executionContext;
executionContext |= RenderContext;
const prevDispatcher = pushDispatcher();
// ......
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// ......
popDispatcher(prevDispatcher);
executionContext = prevExecutionContext;
// Set this to null to indicate there's no in-progress render.
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
// Return the final exit status.
return workInProgressRootExitStatus;
}
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
总体流程与 renderRootSync
一致,只是在 workLoop 函数使用了 workLoopConcurrent
。不再赘述。
另外,我们来探讨下这里的错误处理机制,即 handleError
函数。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function handleError(root, thrownValue): void {
do {
// 当前的 workInProgress 即为出错的 Fiber
let erroredWork = workInProgress;
try {
// ......
if (erroredWork === null || erroredWork.return === null) {
// Expected to be working on a non-root fiber. This is a fatal error
// because there's no ancestor that can handle it; the root is
// supposed to capture all errors that weren't caught by an error
// boundary.
// 这是一个致命错误,因为这是一个没有父节点的 Fiber。因此,此 Fiber 上出现的错误不可冒泡处理。
workInProgressRootExitStatus = RootFatalErrored;
workInProgressRootFatalError = thrownValue;
workInProgress = null;
return;
}
// ......
throwException(
root,
erroredWork.return,
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
// 结束 workLoop
completeUnitOfWork(erroredWork);
} catch (yetAnotherThrownValue) {
// ......
}
// Return to the normal work loop.
return;
} while (true);
}
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
- 如果发生错误的 Fiber 没有父节点则为致命错误,因为无法通过冒泡机制找到捕获错误的目标。
- 如果是普通错误,则结束本次 Fiber Work (当前 Fiber 上的调和工作)。
# workLoopSync
对于同步渲染的 WorkLoop 而言,只需判断 workInProgress
(表示当前正在处理(调和)的 Fiber,即 workInProgress Fiber)不是悬空的即可。不必判断是否是 shouldYield
的时机,因为同步渲染具有最高的优先级,当做 TimeOut 的任务来看待。
提示
悬空本身是指针的概念, workInProgress
表示当前正在处理的 FiberNode 的引用,本质上也是 “指针”,故采用此说法。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// The work loop is an extremely hot path. Tell Closure not to inline it.
/** @noinline */
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
2
3
4
5
6
7
8
9
这里的 while loop
体现的就是 work loop
的思想,即是对 workInProgress FiberTree 数据结构的遍历过程(后文详述), performUnitOfWork
则是体现为在 traverse
的过程中对当前的 FiberNode 进行操作(Work)的过程。
traverse 遍历
traverse
的概念来源于编译原理中 compile
(编译)、 traverse
(遍历)、 generate
(生成)的三个步骤。这里的遍历是指将对 FiberTree 的数据结构进行遍历,并且对 FiberNode 进行处理的过程。
@noinline annotation
编译注解其实就是在编译时进行一些特殊的操作,很多是针对 Java 的概念提出的。注解针对普通的类、变量、方法等,能让编译器支持特殊的操作。注解通常使用的场景是类、方法、字段、局部变量和参数等。
- @inline:标记编译器内联;
- @noinline:标记编译器不要内联,防止因优化器过于智能而过度优化,反而伤害效能。
由于在 WHILE 循环中, performUnitOfWork
会反复被调用,属于是 hot path
, @noinline
的标记告知 JavaScript 编译器,不要将即函数做内联优化处理,以免过度优化伤害程序性能。
参考:
# workLoopConcurrent
对于异步渲染的 WorkLoop 而言,除了需要确保 workInProgress
不能悬空之外,还需要确保调度器没有更高优先级的回调,即 shouldYield
。如果调度器需要打断本次回调,则放弃此次 WorkLoop。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
/** @noinline */
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
2
3
4
5
6
7
8
还需要注意的是,无论是同步渲染还是异步渲染的 WorkLoop 都是通过 performUnitOfWork
来处理的。这是因为所谓同步和异步渲染的区别,归根结底是请求渲染时机的区别,而真正的 WorkLoop 的过程(渲染过程,即调和过程)是一样的。
shouldYield:即 shouldYieldToHost,用于判断是否有任务超时,需要打断调和过程,重新回调。
# performUnitOfWork
在上文中,我们探讨了同步渲染和异步渲染时如何通过调度器的回调(注:此处是简便说法,同步调度实际上是不通过调度器回调的,后文将沿用此说法,且不再重复说明)来启动 WorkLoop 的,而且了解到 WorkLoop 是通过 performUnitOfWork
以在 traverse
过程中对 FiberNode 进行 Work
的。下面我们就来详细探讨 performUnitOfWork
函数,了解此函数是如何遍历 FiberTree 并且对 FiberNode 进行调和的。
// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function performUnitOfWork(unitOfWork: Fiber): void {
// The current, flushed, state of this fiber is the alternate.
// 获取 Fiber 在 currentFiberTree 上的当前渲染版本的 Fiber
const current = unitOfWork.alternate;
// 调和 Fiber,并返回下一个需要调和的 Fiber(DFS)
const next = beginWork(current, unitOfWork, subtreeRenderLanes);
// 将 pendingProps 缓存到 memoizedProps,因为此 Fiber 已经调和完毕
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// If this doesn't spawn new work, complete the current work.
// 如果没有下一个 Fiber 需要调和,则捕获完毕开始冒泡
completeUnitOfWork(unitOfWork);
} else {
// workInProgress 指针移动到下一个需要调和的 Fiber
workInProgress = next;
}
// ReactCurrentOwner.current 是指当前正处于构建过程中的组件。
ReactCurrentOwner.current = null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
有以下的问题需要重点关注一下:
- Fiber 的结构。理解 Fiber 的结构对于理解此函数至关重要。在前文关于
Fiber 和调和基础
的探讨中,我们已经知道了 Fiber 具有如下的结构:
type Fiber = {
// 这个Fiber 的版本池,每个更新的 fiber 都会有一个相对的 alternate fiber。
alternate: Fiber | null,
// Input is the data coming into process this fiber. Arguments. Props.
// 当前 work-in-progress 的组件 props。
pendingProps: any,
// 缓存之前的组件的 props。
memoizedProps: any,
}
2
3
4
5
6
7
8
9
当前的 WorkLoop 处理的是 WorkInProgress FiberTree,因此, unitOfWork
指的是 WorkInProgress FiberNode。另外,根据 FiberTree 的双缓存的结构, unitOfWork.alternate
指向的是 currentFiberTree 上与之相对应的 FiberNode。简单理解之, current
是当前已经渲染的稳定的 FiberNode, unitOfWork
是即将要渲染的需要调和的 FiberNode。
- 调用
beginWork
调和当前 Fiber 节点,completeUnitOfWork
完成 Fiber 的调和过程(从 beginWork 到 completeWork 的过程)。注意next === null
是从捕获到冒泡的转折点,并不是要退出 WorkLoop。 - 指针的移动:
beginWork
会返回下一个需要被调和的 FiberNode,workInProgress
会指向该节点,在 WorkLoop 中继续完成调和过程。如果没有下一个需要调和的节点,说明已经遍历到叶子节点,此时转入冒泡过程,转而执行completeUnitOfWork
。 ReactCurrentOwner.current
的含义:ReactCurrentOwner.current
是指当前正处于构建过程中的组件。这个变量实际上相当于是一个存在于 React 作用域全局的一个缓存变量。- 从
performUnitOfWork
开始,将不在遵循兵分两路的方式,即同步模式和异步模式(上文常提到同步调度和异步调度,同步渲染和异步渲染。)需要注意的是,这里提到的同步和异步表示一种属性而非方式,是一种优先级高低的体现,即调度是同步的或者说渲染是同步的,与编程中同步执行和异步执行
的概念不同。渲染本身并无同步异步之分,渲染的时机(由优先级控制)才有同步和异步之别。
ReactCurrentOwner.current为什么重要?
因为它是自定义节点的指针。所有的 ReactCompositeComponent 最终 render 之后都变成了干干净净的 ReactDomComponent 节点组成的 DOM 树,但是如何分辨哪些是 ReactCompositeComponent 生成的呢?这就依赖这些 ReactDomComponent 节点上的 owner 变量。而 ReactCurrentOwner.current
正是维护这个在构建虚拟 DOM 过程中,随时会变动的变量的临时保存位置所在。
这个值会被缓存到 ReactElement.__owner 中。
参考:
# 扩展
# 如何理解 WorkLoop?
从过程来来看,WorkLoop 是对 workInProgress FiberTree 的遍历与回溯(捕获和冒泡)的过程,在此二者过程中,分别对 FiberNode 做 beginWork 和 completeWork 的工作,以达到挂载、更新和标记 EffectTag
(后文可能会直接称之为 ETag) 的目的。
从功能上来看,WorkLoop 的目的是对 workInProgress FiberTree 进行调和(针对 VDOM 的组件的挂载和更新,针对 DOM 转化的 ETag 的标记),这是一次从 workInProgress FiberTree 到 current FiberTree 的构造、加工和飞跃的过程。WorkLoop 的工作是 Render
阶段的核心工作,也是实质性的工作,这为 fiberRoot.current
的迁移工作打下了夯实的基础。
下面是一次从 RootFiber 开始的 WorkLoop 的过程,您可以根据此图了解 FiberTree 的结构以及 WorkLoop 捕获与冒泡的过程。关于 WorkLoop
的详细的工作流程,在后文中会进行更详细的探讨。
# 如何理解 workLoop
和 performUnitOfWork
的关系?
workLoop 是整个调和工作的控制器,是控制遍历 FiberTree 的引擎,也可以称之为调和工厂,相应的, performUnitOfWork
是调和 FiberNode 的具体工作者,也可以称之为调和工作的工人,具体控制着 beginWork
和 completeWork
的职责。二者在 FiberTree 这个指针结构的协助下,共同完成调和 FiberTree 的任务。
在 performUnitOfWork
和 workLoopConcurrent
中添加 @noinline
的非内联标记,也能体现这一思想。因为对于 workLoop
而言, performUnitOfWork
必然是多实例的,内联编译则破坏了这一思路。虽然非内联函数在函数的启动、缓存时必然耗费了更多的内存,但是这样反而是正确的方式,这是因为空间换时间的方式能够提高程序的执行效率。
# 问题
# workInProgress 是如何初始化的?
我们可能会有这样的疑问, workInProgress
在应用挂载时是悬空的,而在 WorkLoop
中确是对 workInProgress
进行操作,那么 workInProgress
是如何初始化的呢?
事实上,在 renderRootSync
和 renderRootConcurrent
中都有如下的一段逻辑:
// If the root or lanes have changed, throw out the existing stack
// and prepare a fresh one. Otherwise we'll continue where we left off.
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// ......
prepareFreshStack(root, lanes);
}
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
// ......
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
// ......
}
2
3
4
5
6
7
8
9
10
11
12
13
14
如果 workInProgressRoot
即当前处理的 FiberTree 的根节点(或者渲染优先级)发生了变化,则清理 workInProgressStack
的内容,包括重新创建 workInProgress
节点。因此在应用挂载时, workInProgress
实际上是根据 root.current
的 FiberNode 而创建的。
# 总结
通过本篇文章的探讨,有如下的重点内容需要关注:
Render
的过程本质上是WorkLoop
的过程,WorkLoop
的错误处理具有较好的容错度。workLoopSync
和workLoopConcurrent
本质上都是捕获和冒泡调和 FiberTree 的过程, 也都是通过performUnitOfWork
函数调和 FiberNode。二者唯一的不同是跳出时机的不同,workLoopConcurrent
除了要判断workInProgress
未悬空之外,还需要判断调度器是否需要打断调和过程。performUnitOfWork
独立控制着beginWork
和completeWork
即completeUnitOfWork
的流程。