Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • plan 计划
  • 基础

  • 调和(Reconciliation)

  • 调度器(Scheduler)

  • 更新器(Updater)

  • 渲染器(Render)

  • hooks原理

  • 总结

  • React源码漂流记

    • 开始上手
    • Plan 计划
    • 前言
    • React 源码漂流记:ReactElement 与基础概念
    • React 源码漂流记:ReactChildren 与节点操纵
    • React 源码漂流记:React 整体结构和理念初认识
    • React 源码漂流记:React 调和器核心源码解读(一)
    • React 源码漂流记:React 调和器核心源码解读(二)
    • React 源码漂流记:React 调和器核心源码解读(三)
      • 目录
      • 前言
      • renderRootSync
      • renderRootConcurrent
      • workLoopSync
      • workLoopConcurrent
      • performUnitOfWork
      • 扩展
      • 问题
      • 总结
    • React 源码漂流记:React 调和器核心源码解读(四)
    • React 源码漂流记:React 调和器核心源码解读(五)
    • React 源码漂流记:React 调和器核心源码解读(六)
    • React 源码漂流记:React 调和器核心源码解读(七)
    • React 源码漂流记:React 调和器核心源码解读(八)
    • React 源码漂流记:React 调和器核心源码解读(九)
    • React 源码漂流记:React 调和器核心源码解读(十)
    • React 源码漂流记:React 调度器核心源码解读(一)
    • 带着原理重读 React 官方文档(一)
    • 带着原理重读 React 官方文档(二)
  • react
  • React源码漂流记
jonsam
2022-07-12
目录

React 源码漂流记:React 调和器核心源码解读(三)

标签: React17精简

# 目录

  • 目录
  • 前言
  • renderRootSync
  • renderRootConcurrent
  • workLoopSync
  • workLoopConcurrent
  • performUnitOfWork
  • 扩展
    • 如何理解 WorkLoop?
    • 如何理解 workLoop 和 performUnitOfWork 的关系?
  • 问题
    • workInProgress 是如何初始化的?
  • 总结

# 前言

在上一篇文章中,我们探讨了 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;
}
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

这里有几点核心的内容:

  • 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;
}
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

总体流程与 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);
}
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
  • 如果发生错误的 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);
  }
}
1
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 编译器,不要将即函数做内联优化处理,以免过度优化伤害程序性能。

参考:

  • Automatic Inlining in JavaScript Engines · ariya.io (opens new window)
  • Optimizing for V8 - Inlining, Deoptimizations | Codegen::RecordSafepoint (opens new window)

# 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);
  }
}
1
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

有以下的问题需要重点关注一下:

  1. 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,
}
1
2
3
4
5
6
7
8
9

当前的 WorkLoop 处理的是 WorkInProgress FiberTree,因此, unitOfWork 指的是 WorkInProgress FiberNode。另外,根据 FiberTree 的双缓存的结构, unitOfWork.alternate 指向的是 currentFiberTree 上与之相对应的 FiberNode。简单理解之, current 是当前已经渲染的稳定的 FiberNode, unitOfWork 是即将要渲染的需要调和的 FiberNode。

  1. 调用 beginWork 调和当前 Fiber 节点, completeUnitOfWork 完成 Fiber 的调和过程(从 beginWork 到 completeWork 的过程)。注意 next === null 是从捕获到冒泡的转折点,并不是要退出 WorkLoop。
  2. 指针的移动: beginWork 会返回下一个需要被调和的 FiberNode, workInProgress 会指向该节点,在 WorkLoop 中继续完成调和过程。如果没有下一个需要调和的节点,说明已经遍历到叶子节点,此时转入冒泡过程,转而执行 completeUnitOfWork 。
  3. ReactCurrentOwner.current 的含义: ReactCurrentOwner.current 是指当前正处于构建过程中的组件。这个变量实际上相当于是一个存在于 React 作用域全局的一个缓存变量。
  4. 从 performUnitOfWork 开始,将不在遵循兵分两路的方式,即同步模式和异步模式(上文常提到同步调度和异步调度,同步渲染和异步渲染。)需要注意的是,这里提到的同步和异步表示一种属性而非方式,是一种优先级高低的体现,即调度是同步的或者说渲染是同步的,与编程中 同步执行和异步执行 的概念不同。渲染本身并无同步异步之分,渲染的时机(由优先级控制)才有同步和异步之别。

ReactCurrentOwner.current为什么重要?

因为它是自定义节点的指针。所有的 ReactCompositeComponent 最终 render 之后都变成了干干净净的 ReactDomComponent 节点组成的 DOM 树,但是如何分辨哪些是 ReactCompositeComponent 生成的呢?这就依赖这些 ReactDomComponent 节点上的 owner 变量。而 ReactCurrentOwner.current 正是维护这个在构建虚拟 DOM 过程中,随时会变动的变量的临时保存位置所在。

这个值会被缓存到 ReactElement.__owner 中。

参考:

  • React ReactCurrentOwner | Que's Blog (opens new window)
  • _owner 是如何连接 ReactElement 和 Fiber 的?_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 的详细的工作流程,在后文中会进行更详细的探讨。

search-react-code

# 如何理解 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;
  // ......
}
1
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 的流程。
编辑 (opens new window)
上次更新: 2022/07/28, 09:19:48
React 源码漂流记:React 调和器核心源码解读(二)
React 源码漂流记:React 调和器核心源码解读(四)

← React 源码漂流记:React 调和器核心源码解读(二) React 源码漂流记:React 调和器核心源码解读(四)→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式