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 调和器核心源码解读(三)
    • React 源码漂流记:React 调和器核心源码解读(四)
    • React 源码漂流记:React 调和器核心源码解读(五)
    • React 源码漂流记:React 调和器核心源码解读(六)
      • 目录
      • 前言
      • completeUnitOfWork
      • completeWork
      • bubbleProperties
      • Render 阶段的终结
      • 扩展
      • 问题
      • 总结
      • 参考
    • React 源码漂流记:React 调和器核心源码解读(七)
    • React 源码漂流记:React 调和器核心源码解读(八)
    • React 源码漂流记:React 调和器核心源码解读(九)
    • React 源码漂流记:React 调和器核心源码解读(十)
    • React 源码漂流记:React 调度器核心源码解读(一)
    • 带着原理重读 React 官方文档(一)
    • 带着原理重读 React 官方文档(二)
  • react
  • React源码漂流记
jonsam
2022-07-30
目录

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

标签: React17精简

# 目录

  • 目录
  • 前言
  • completeUnitOfWork
  • completeWork
  • bubbleProperties
  • Render 阶段的终结
  • 扩展
    • bubbleProperties 中为什么要收集 subtreeFlags?
  • 问题
  • 总结
  • 参考

# 前言

在上文中,我们探讨了 React 调和过程中捕获的详细过程和原理。在整体上而言,捕获过程分成两个步骤,“调和当前节点” 和 “调和子节点”,“调和当前节点” 就是对 workInProgress 的节点( IndeterminateComponent 、 LazyComponent 、 FunctionComponent 、 ClassComponent 等)进行调和,不同的组件类型有不同的调和策略,“调和子节点” 就是对将 JSX 榨取出的 ReactElement 作为子节点进行调和,这其中大致又可分成两个步骤,即所谓 “调和” 和 “置位”,其中调和就是针对旧节点是否可复用而采取复用或者新建的方式获取子节点,应用最新的 state 和 props 等,最新获取整个调和完毕的 FiberTree,另外,“置位” 就是针对子节点标记副作用,这对本文或者后文中所述的副作用收集、DOM 的更新至关重要。

在本文中,我们回转头来,从 “捕获过程” 的细节万花筒的浮出,继续深入探索 React 调和中的 “冒泡过程”。

还记得 “捕获过程” 转换到 “冒泡过程” 的契机在哪里吗?

在 performUnitOfWork (opens new window) 函数的分析中,有如下的关键代码:

const next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
  completeUnitOfWork(unitOfWork);
} else {
  workInProgress = next;
}
1
2
3
4
5
6

在前文中我们已经知道,在捕获过程中,如果深入到叶子节点(没有子节点的节点)或者是因无更新而提前结束捕获过程时,都是将 next 悬空并返回,因此在函数 performUnitOfWork 中如果遇到 next 指针悬空的情况,则意味着捕获过程暂时结束了,可以进行冒泡了。 unitOfWork 本身就是 workInProgress 的节点。因此 completeUnitOfWork 函数即可看做是冒泡过程的入口。而本文的探讨也将由此展开。

注意

由捕获转到冒泡并不意味着不会再进行捕获。事实上,捕获冒泡的过程是对 FiberTree 的遍历的过程,因此,在冒泡时(优先遍历 sibling 节点,return 节点次之)遇到新的非叶子结点时,会再次转换到捕获过程。更多细节可参考如何理解 WorkLoop? (opens new window)。

# completeUnitOfWork

标签: 重要

此函数在前置工作 beginWork 之后处理当前节点(workInProgress)的后置工作,即 completeWork 。同时与 performUnitOfWork 配合,共同完成 FiberTree 的遍历过程。

// src/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function completeUnitOfWork(unitOfWork: Fiber): void {
  // Attempt to complete the current unit of work, then move to the next
  // sibling. If there are no more siblings, return to the parent fiber.
  let completedWork = unitOfWork;
  do {
    // 从 workInProgress Fiber 的 alternate 指针获取到 current Fiber
    const current = completedWork.alternate;
    // 从 workInProgress Fiber 的 return 指针获取到父节点
    const returnFiber = completedWork.return;

    // 检查 flags 上是否有 Incomplete 标记,无此标记即为没有未完成的工作(没有中断或者抛出错误)
    if ((completedWork.flags & Incomplete) === NoFlags) {
      // ......
      // completeWork 在当前节点上完成工作,并且返回下一个节点(大多数情况下 next 都是 null)
      const next = completeWork(current, completedWork, subtreeRenderLanes);
      // workInProgress 指向下一个节点
      if (next !== null) {
        workInProgress = next;
        return;
      }
    } else {
      // Fiber 上前述任务未完成,可能有异常抛出,此时清理相关堆栈,进行错误冒泡捕获
      // unwindWork 在少数清空下回抛出下个任务,如果抛出则继续执行此任务
      // 注意,这里 return 后续才有可能有捕获过程
      const next = unwindWork(completedWork, subtreeRenderLanes);
      if (next !== null) {
        next.flags &= HostEffectMask;
        workInProgress = next;
        return;
      }
      // ......
      // 因当前 returnFiber 为 Incomplete 状态,因此添加相关标记
      if (returnFiber !== null) {
        // Mark the parent fiber as incomplete and clear its subtree flags.
        returnFiber.flags |= Incomplete;
        returnFiber.subtreeFlags = NoFlags;
        returnFiber.deletions = null;
      }
    }
    // 有兄弟节点时,移动到兄弟节点(有可能 beginWork 或者 completeWork)
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    // 无兄弟节点时移动到父节点,将 completedWork 和 workInProgress 指针均移动到父节点
    // 注意:这里并没有 return,这是因为父节点不需要重复捕获,继续冒泡即可,等移动到父节点的兄弟节点在考虑是否需要捕获
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);

  // 只要有兄弟节点或者父节点,上述循环就不会停止,如果退出循环,说明到达根节点
  // 将退出状态改为 `RootCompleted`
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}
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
56
57
58

分析如下:

  • 前文中在 performUnitOfWork 中不断向子节点深入, completeUnitOfWork 中通过循环不断向兄弟节点扩展,这充分体现了 DFS 的原理。其中从代码逻辑上,也可以看出控制 child 、 sibling 、 return 三个指针的移动也是遵循一定的优先级的,DFS 所要求的优先级正是: child -> sibling -> return 。
  • 在当前节点执行 completeUnitOfWork 时,会根据节点上是否有 Incomplete 标记(是否 beginWork 的前置任务未完成),选择不同的策略。正常情况下,执行 completeWork 来完成节点上后续任务。
  • 当 WorkLoop 遍历完毕返回到根节点时,将退出状态改成 RootCompleted 。此状态将在 renderRootSync 或者 renderRootConcurrent 函数中被返回。在函数 performSyncWorkOnRoot 或者 performConcurrentWorkOnRoot 中,结束 Render 阶段进行 Commit 阶段。
  • workInProgressRootExitStatus 表示 workInProgress 的 RootFiber 在退出时的状态。WorkLoop 所展开的 “捕获与冒泡” 的遍历过程,最终会返回到根节点,此时整个过程执行的状态就标记于此。

扩展

RootExitStatus 有如下的状态:

type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
const RootIncomplete = 0; // 任务尚未完成
const RootFatalErrored = 1; // 发生致命错误
const RootErrored = 2; // 发生错误
const RootSuspended = 3; // 任务被暂停
const RootSuspendedWithDelay = 4; // 任务被延迟
const RootCompleted = 5; // 任务已经完成
1
2
3
4
5
6
7

# completeWork

从 completeUnitOfWork 函数中,我们已经获知, completeWork 事实上是为了完成 workInProgress Fiber 节点上的后置工作。那么具体要哪些工作呢?我们继续往下看:

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  // ......
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      bubbleProperties(workInProgress);
      return null;
    case ClassComponent: {
      // ......
      bubbleProperties(workInProgress);
      return null;
    }
    case HostRoot: {
      // ......
      bubbleProperties(workInProgress);
      return null;
    }
    case HostComponent: {
      // ......
      bubbleProperties(workInProgress);
      return null;
    }
    case HostText: {
      // ......
      bubbleProperties(workInProgress);
      return null;
    }
    case SuspenseComponent: /*......*/
    case HostPortal:
      // ......
      bubbleProperties(workInProgress);
      return null;
    case ContextProvider:
      // ......
      bubbleProperties(workInProgress);
      return null;
    case IncompleteClassComponent: {
      // ......
      bubbleProperties(workInProgress);
      return null;
    }
    case SuspenseListComponent:  /*......*/
    case ScopeComponent:  /*......*/
    case OffscreenComponent:
    case LegacyHiddenComponent:  /*......*/
    case CacheComponent: /*......*/
  }
  // ......
}
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
56
57
58
59
60
61
62

由上大致可以看出,最重要的内容是要执行 bubbleProperties 函数。其余细节不在赘述。

# bubbleProperties

此函数的目的是通过遍历节点下的下级子节点以收集节点的 childLanes 和 subtreeFlags 。结合整个冒泡过程来看,这实际上从 FiberTree 的底部向上冒泡以不断的更新属性。 childLanes 和 subtreeFlags 本别对应着节点的优先级和服副作用标记,都是极其重要的属性。

function bubbleProperties(completedWork: Fiber) {
  // 判断是否是根据 bailoutOnAlreadyFinishedWork 结束 beginWork 的
  // child 相等说明复用了子树,见 cloneChildFibers
  const didBailout =
    completedWork.alternate !== null &&
    completedWork.alternate.child === completedWork.child;
  // ......
  let newChildLanes = NoLanes;
  let subtreeFlags = NoFlags;
  let child = completedWork.child;
  while (child !== null) {
    newChildLanes = mergeLanes(
      newChildLanes,
      mergeLanes(child.lanes, child.childLanes),
    );
    subtreeFlags |= child.subtreeFlags;
    subtreeFlags |= child.flags;
    child.return = completedWork;
    child = child.sibling;
  }

  completedWork.subtreeFlags |= subtreeFlags;
  completedWork.childLanes = newChildLanes;

  return didBailout;
}
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

参见如下分析:

  • didBailout 是判断是否是通过 bailout (中文意思是 “保释”) 的方式退出捕获过程的, bailout 的方式和正常方式在此处有细微的区分,但是可以忽略。

# Render 阶段的终结

Render 阶段的终结,就意味着 Commit 阶段的开始。我们已经知道了一个正常的(忽略错误情况)渲染周期包括 Batch 阶段、 Render 阶段和 Commit 阶段,也就是所谓的 ExecutionContext 。 Batch 阶段主要是接受更新和调度请求, Render 阶段主要是对调度的回调做出反应,开启渲染(调和)的过程, Commit 阶段对 Render 阶段的调和结果进行确认,并且将更新落实到不同的 Host 宿主环境中,如 Web 环境、SSR 环境。

React 应用生命的大部分时间都是处于 batch 阶段,只有少部分时间切片分配给了 Render 阶段,并且 Render 阶段还可以被调度器通过 yield 的方式打断,极少部分的时间处于 Commit 阶段,且 Commit 阶段是不可打断的。这种情况也符合视图渲染框架的要求预期,因为 Batch 阶段才是最稳定的,是常态的; Render 的过程在调度器回调后产生,渲染的频率应当是高效且可控的,要把握更新频率和性能损耗的一个平衡(这里可以理解为刷新率的概念,后文我们将在调度器中体会这一点),最后 Commit 阶段是最不稳定的,因为这里涉及到 workInProgress FiberTree 和 current FiberTree 的交换过程,应当确保这个过程尽快完成,以免影响到页面渲染的平滑性。可以参考下图加深理解:

batch-render-commit

本文分析到这里, Render 阶段大致就告一段落,更深入的细节可以另行探讨,但是整体的调和的脉络必须向前推进了。下面我们在来重温下此两阶段切换的细节。在 performSyncWorkOnRoot 和 performConcurrentWorkOnRoot 函数(参见:React 调和器核心源码解读(二) (opens new window))中有如下代码:

// performSyncWorkOnRoot
let exitStatus = renderRootSync(root, lanes);
// performConcurrentWorkOnRoot
let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);

const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// performSyncWorkOnRoot
commitRoot(root);
// performConcurrentWorkOnRoot
finishConcurrentRender(root, exitStatus, lanes);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

我们已经知道了,上述两个函数实际上控制着 Render 过程和 Commit 过程,在 Render 结束之后,最重要的是产生了 finishedWork 和 finishedLanes ,分别代表着调和完毕的 workInProgress FiberTree 和 lanes(优先级),在后文的 Commit 过程中,将会对此做进一步的处理。

另外还有一点需要注意的是,React 整体的生命周期(注意,这里不是指组件的生命周期,而是指上述的三个阶段)的控制是在 root ,即 FiberRoot 容器上控制的,这也是由双缓存结构所决定的,因为 FiberRoot 上 current 和 finishedWork 指针分别指向两棵 FiberTree。相对应的 current 节点和 workInProgress 节点之间是通过 alternate 指针相连接的。

在下一篇文章中,我们将继续探讨 Commit 阶段的原理,拭目以待吧。

# 扩展

# bubbleProperties 中为什么要收集 subtreeFlags ?

在 React 旧版本中,在捕获时会将 Fiber 节点上的 EffectTag 进行收集,形成 Effect List 链表,因此,在 Commit 时,只需遍历 Effect List 链表,对链表中的副作用执行相应的 mutation 操作。在新版中 React 中去除了 Effect List 的概念,使用 subtreeFlags 的概念。 subtreeFlags 将子树的的 EffectTag 通过冒泡的方式收集到父节点(实际上是组件节点,参见 completeWork 函数)上,在 Commit 时,再根据收集到的 subtreeFlags 遍历子树,为子节点执行相应的 mutation 操作。

具体的细节可以参考:React Effects List 大重构,是为了他? (opens new window)。

# 问题

# 总结

本文主要讲述了 WorkLoop 过程中 “捕获” 与 “冒泡” 机理,以及在冒泡过程中所做的重要的后置工作。

  • completeUnitOfWork 函数的核心职责是控制冒泡的过程以及完成在冒泡过程中的后置工作。“冒泡” 是从当前节点向兄弟节点或者是父节点移动的过程,在整个 WorkLoop 中会遍历到每一个节点,并且在节点上一次执行 beginWork 和 completeWork 。 beginWork 执行的工作可称之为前置工作,主要是 Fiber 节点的调和和 EffectTag 的标记; completeWork 中执行的工作可称之为后置工作,主要是优先级和副作用的收集和更新。

# 参考

编辑 (opens new window)
上次更新: 2022/08/01, 20:37:47
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 ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式