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 调和器核心源码解读(六)
    • React 源码漂流记:React 调和器核心源码解读(七)
    • React 源码漂流记:React 调和器核心源码解读(八)
      • 目录
      • 前言
      • commitBeforeMutationEffectsOnFiber
      • commitMutationEffectsOnFiber
      • commitLayoutEffectOnFiber
      • 扩展
      • 问题
      • 总结
      • 参考
    • React 源码漂流记:React 调和器核心源码解读(九)
    • React 源码漂流记:React 调和器核心源码解读(十)
    • React 源码漂流记:React 调度器核心源码解读(一)
    • 带着原理重读 React 官方文档(一)
    • 带着原理重读 React 官方文档(二)
  • react
  • React源码漂流记
jonsam
2022-08-05
目录

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

标签: React17精简

# 目录

  • 目录
  • 前言
  • commitBeforeMutationEffectsOnFiber
  • commitMutationEffectsOnFiber
  • commitLayoutEffectOnFiber
  • 扩展
    • useLayoutEffect 中 layout 含义是什么?
  • 问题
  • 总结
  • 参考

# 前言

在上文中,我们探讨了 Commit 过程的三大步骤,以及完成这三大步骤所采用的遍历方式。 beforeMutation 、 mutation 和 layout 此三大步骤归根结底是对 workInProgress FiberTree 的应用,是两个重要跳跃的基石:一是从 WorkInProgress FiberTree 向 current FiberTree 的跳跃,二是从 EffectList 向 DOM 更新 的跳跃。

扩展来说,有以下几点值得我们思考:

  • FiberTree 、 ReactElementTree 和 DOMTree 在 React 中的关系是相当复杂的。总结来说, DOMTree 是页面视图的状态, ReactElementTree 是逻辑视图的状态,它包含了组件层面的状态变化(setState)、视图更新(JSX 更新)和事件响应(Event Listener),而 FiberTree 是数据层面的状态,它是应用层面的(或者说是 FiberRoot 容器层面的)数据的生态,是对状态变化(updateQueue)、视图更新(EffectList)、事件响应(事件委托系统)的数据抽象。我们可以从整个调和器的大循环中进行体会。
  • “捕获和冒泡” 是 React 中针对 Tree 数据结构的一种通用的遍历方式,其本质是 DFS(深度优先遍历)的模型,React 将调和 FiberTree、消费 EffectList 的逻辑注入到 DFS 的过程之中,并针对 Tree 结构的特性进行性能和效率的优化。为什么采用 DFS 的方式呢?一是足够高效,DFS 对每个节点访问(visit )两次;而是足够灵活,DFS 可以跳过某些不需要遍历的子树从而提升遍历效率。

# commitBeforeMutationEffectsOnFiber

此函数在 mutation 阶段之前执行类组件的 getSnapshotBeforeUpdate 函数。

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  // ......
  // Snapshot EffectTag 标记在具有 getSnapshotBeforeUpdate 函数的类组件上或者 `HostRoot` 上
  if ((flags & Snapshot) !== NoFlags) {
    // ......
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        break;
      }
      case ClassComponent: {
        // 如果非首次渲染
        if (current !== null) {
          // 当前 current Fiber 上的 prop、state 和组件实例
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          // ......
          // 调用 getSnapshotBeforeUpdate,see https://zh-hans.reactjs.org/docs/react-component.html#getsnapshotbeforeupdate
          const snapshot = instance.getSnapshotBeforeUpdate(
            // 如果 elementType 和 type 不一致,则可能是 lazyComponent,需要
            // 将 ReactElement 上的默认 props 同步到组件实例上,see https://zh-hans.reactjs.org/docs/react-component.html#defaultprops
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          // ......
          // 缓存 snapshot 值以在 componentDidUpdate 中使用
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
      case HostRoot: {
        // 如果是 HostRoot 组件,说明是首次渲染,清空容器
        const root = finishedWork.stateNode;
        clearContainer(root.containerInfo);
        break;
      }
      case HostComponent:
      case HostText:
      case HostPortal:
      case IncompleteClassComponent:
        break;
      // ......
    }
  }
}
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

大家可能已经注意到了,如上的代码都是针对有 Snapshot 标记的 fiber 节点执行的, Snapshot 标记用于有 getSnapshotBeforeUpdate 的函数的类组件和 HostRoot 组件,用于对需要在 DOM 操作之前操作原来的实例信息、DOM 节点的场景做标记。 HostRoot 节点是 ReactDOM.render 中挂载到 root 容器中的 RootFiber 节点,一般情况下 HostRoot 在非首次渲染时并不会发生变化,至首次渲染时则需要对对所挂载的容器进行节点清空。

此函数有如下作用:

  • 针对类组件,执行 getSnapshotBeforeUpdate(prevProps, prevState) 生命周期函数,并且将 snapshot 传递给 componentDidUpdate(prevProps, prevState, snapshot) 。
  • 针对 HostRoot 节点,清空 root 容器中的 DOM 节点。

# commitMutationEffectsOnFiber

此函数对移位(placement)、update(更新)等操作提交相应的 mutation 操作,此 mutation 操作将会操纵 JavaScript 进行 DOM 节点的更新。

function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
  const flags = finishedWork.flags;
  // 如果有 ContentReset 标记,清空节点中的文本内容
  if (flags & ContentReset) {
    commitResetTextContent(finishedWork);
  }
  // 如果有 Ref 标记,则将对应的 current 节点上的 Ref 关联去除
  if (flags & Ref) {
    const current = finishedWork.alternate;
    if (current !== null) {
      commitDetachRef(current);
    }
    // ......
  }
  // ......

  // The following switch statement is only concerned about placement,
  // updates, and deletions. To avoid needing to add a case for every possible
  // bitmap value, we remove the secondary effects from the effect tag and
  // switch on that value.
  // 由于下面的处理只关心 placement、updates 和 deletions 相关的操作,因此将之作为一流标记保留,其余标记均删除
  const primaryFlags = flags & (Placement | Update | Hydrating);
  outer: switch (primaryFlags) {
    // 如果包含 Placement(位置变化)标记,则提交 Placement 的 mutation 操作
    case Placement: {
      commitPlacement(finishedWork);
      // Clear the "placement" from effect tag so that we know that this is
      // inserted, before any life-cycles like componentDidMount gets called.
      // 清理标记
      finishedWork.flags &= ~Placement;
      break;
    }
    // 如果包含 PlacementAndUpdate(位置变化和节点更新)标记,则提交 Placement 和 update 的 mutation 操作
    case PlacementAndUpdate: {
      // Placement
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;
      // 提交 update 操作
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
    case Hydrating: {
      // SSR 无需 DOM 操作
      finishedWork.flags &= ~Hydrating;
      break;
    }
    case HydratingAndUpdate: {
      finishedWork.flags &= ~Hydrating;
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
    case Update: {
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
  }
}
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

此函数核心作用是对具有 primaryFlags(一流的标记)的节点提交相应的 mutation 操作。细节内容如下:

  • 处理 ContentReset 和 Ref 标记。如果节点有 ContentReset 标记,则清空节点内部的文本内容,如果节点有 Ref 标记,则解除该节点相对应的 current 节点上 Ref 的联结(注意当前节点上并没有 Ref 的联结),因为节点在 layout 过程中会重新建立 Ref 联结。Ref 的原理将在 hook 相关章节详述。
  • commitMutationEffectsOnFiber 只关心与 placement、update 和 hydrating(水合)相关的遗留标记(注意:删除操作已经移到冒泡过程中处理,此处不再处理), primaryFlags 的位运算计算原理不再赘述。需要特别交代的是:为什么 PlacementAndUpdate 和 PlacementAndUpdate 也能被处理,事实上这两个标记是复合标记,见下:
const PlacementAndUpdate = Placement | Update;
const HydratingAndUpdate = Hydrating | Update;
1
2
  • 如果有 Placement 标记,则调用 commitPlacement 提交移位的 mutation 操作。
  • 如果有 Update 标记,则调用 commitWork 提交更新的 mutation 操作。

# commitLayoutEffectOnFiber

这个函数主要是针对不同的组件类型执行不同的处理,包括生命周期的处理、副作用的处理等。

// src/react/packages/react-reconciler/src/ReactFiberCommitWork.new.js
const LayoutMask = Update | Callback | Ref | Visibility;

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot, 
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        // ......
        // 调用 useLayoutEffect 的回调函数,并且缓存销毁函数
        commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
        break;
      }
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        // 如果组件发生了更新
        if (finishedWork.flags & Update) {
          // 如果有 `Update` 标记,在首次渲染时执行实例的 componentDidMount 生命周期函数,
          // 否则执行 componentDidUpdate 生命周期函数
          if (current === null) {
            // ......
            instance.componentDidMount();
          } else {
            // 因为此处 workProgress 已经与 current 交换,所以 current 上具有最新的 props 和 state
            const prevProps =
              finishedWork.elementType === finishedWork.type
                ? current.memoizedProps
                : resolveDefaultProps(
                    finishedWork.type,
                    current.memoizedProps,
                  );
            const prevState = current.memoizedState;
            // ......
            // 成为了 current fiber 之后,相对于 workInProgress fiber 而言,其 props 和 state 就是之前的。
            // see https://zh-hans.reactjs.org/docs/react-component.html#componentdidupdate
            instance.componentDidUpdate(
              prevProps,
              prevState,
              instance.__reactInternalSnapshotBeforeUpdate, // getSnapshotBeforeUpdate 返回的 snapshot
            );
          }
        }
        const updateQueue: UpdateQueue<*,> | null = (finishedWork.updateQueue: any);
        if (updateQueue !== null) {
          // ......
          // 消费 updateQueue 中的副作用回调
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostRoot: {
        // 如果 HostRoot 上有 updateQueue(注意 updateQueue 是一个 object),可能有 callback effect,因此
        // 提交 commitUpdateQueue 消费这些 effect,注意这里传递给 effect 的 context 是 HostRoot 下直系的的叶子节点
        const updateQueue: UpdateQueue<*,> | null = (finishedWork.updateQueue: any);
        if (updateQueue !== null) {
          let instance = null;
          if (finishedWork.child !== null) {
            switch (finishedWork.child.tag) {
              case HostComponent:
                // getPublicInstance 兼容不同 HOST 环境
                instance = getPublicInstance(finishedWork.child.stateNode);
                break;
              case ClassComponent:
                instance = finishedWork.child.stateNode;
                break;
            }
          }
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostComponent:
      case HostText: 
      case HostPortal:
      case Profiler: 
      case SuspenseComponent: 
      case SuspenseListComponent:
      case IncompleteClassComponent:
      case ScopeComponent:
      case OffscreenComponent:
      case LegacyHiddenComponent:
        break;
      // ......
    }
  }

  // ......
  // 如果 fiber 上有 Ref 标记,则重新建立 Ref 联结
  if (finishedWork.flags & Ref) {
    commitAttachRef(finishedWork);
  }
}
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

分析如下:

注意此函数的执行时机, mutation 阶段之后表示 DOM 的更改已经提交了, requestPaint 之前,表示浏览器很有可能并没有实现视图的绘制,但是这不影响相关的生命周期函数在 layout 期间可以获取到最新的 DOM 属性、组件状态和属性。

  • 对于 FunctionComponent 、 ForwardRef (接受一个参数 render,即为函数式组件或者类组件的 render 函数,返回一个可传递 Refs 的函数)、 SimpleMemoComponent (接受一个函数式组件,返回一个可缓存 props(浅比较) 的函数式组件) 这些函数式组件而言,执行 commitHookEffectListMount 处理 useLayoutEffect 中的副作用回调,并且缓存其销毁回调。
  • 对于 ClassComponent 而言,需要处理类组件的生命周期函数,如果是初次渲染则调用 componentDidMount ,否则就调用 componentDidUpdate 。 commitUpdateQueue 会消费掉 setState 中传入的回调函数,即 setState(updater[, callback]) 中的 callback。因此可以看出,这些 setState 中的回调并不是在 updater 执行后调用的,而是收集起来在 layout 阶段批量消费的,这样可以保证 callback 执行之时 state 是已经更新过的,也可以提高回调执行的效率(事实上,state 的更新也是批量完成的)。
  • 对于 HostRoot (RootFiber) 而言,也需要执行 commitUpdateQueue 消费回调,此回调来自于 render(element, container[, callback]) 中的 callback 。 Callback 标记不止用类组件中 setState 的回调,也用于 HostRoot 上 render 或者 hydrate 的回调。

# 扩展

# useLayoutEffect 中 layout 含义是什么?

此 layout 正是来源于 layout 阶段中的这个 layout 的概念,中文意为 “布局”。我们可以结合 commitLayoutEffectOnFiber 这个函数的具体内容来分析 layout 的含义。其实,无论是 useLayoutEffect 的处理,还是类组件生命周期钩子 componentDidMount 或 componentDidUpdate 的处理,还是对各种 Callback 副作用的处理,都离不开一个词,即是 “副作用”。具体而言, layout 所处理的正是组件挂载(或者更新)时的各种同步的副作用。这些副作用依赖于 mutation 阶段完成 DOM 操作这个时机,对组件的状态和行为产生一定的影响,进而影响对组件后续的渲染。

因为 layout 中副作用的执行是同步的、阻塞渲染的,因此也就可以在渲染之前对 DOM 进行更改,从而使浏览器可以一并完成重排和重绘。从这个角度来看,颇有 “布局” 的含义。以 DOM 更新的视角而言, useLayoutEffect 可以减少浏览器的绘制成本,如果不涉及到阻塞更新的缺陷(具有较小的阻塞成本,并且多为 DOM 操作),则可以考虑使用 useLayoutEffect 。

# 问题

# 总结

本文承接上文中 EffectList 循环的内容,讲解在其循环过程中的 “冒泡” 阶段所执行的三个核心函数的内容。现对此三个函数总结如下:

  • commitBeforeMutationEffectsOnFiber : 执行于 beforeMutation 阶段,主要是针对具有 Snapshot 标记的节点做处理。 Snapshot 意为 “快照”,在 mutation 阶段变回执行真正的 DOM 操作,因此趁此 mutation 未处理尚可以操作旧 DOM 节点之际,对依靠 Snapshot 的相关逻辑进行处理。例如类组件 getSnapshotBeforeUpdate 钩子函数。
  • commitMutationEffectsOnFiber : 执行于 mutation 阶段,主要对具有 Update 和 Placement 标记的节点进行处理。 Update 和 Placement 分别对应节点的 “更新” 和 “替换” 行为,因此此过程主要对节点进行 DOM 修正(即 mutation )。由于组件是 DOM 节点组织形式的抽象,因此无论是函数式组件、类组件还是其他,一律不考虑组件类型,提交相应的 mutation 操作即可(事实上,在提交此操作后真正执行修正过程中才会针对组件类型进行区分处理)。
  • commitLayoutEffectOnFiber : 执行于 layout 阶段,主要针对有 LayoutMask 标记的节点做处理(注意:实际上没有这个标记,这是一个标记集合,或者成为遮罩标记)。由于位于 mutation 阶段之后,因此此过程主要对依赖于新的 DOM 状态或者组件挂载(或者更新)的逻辑进行处理。其中比较重要的包括三个方面:
    1. useLayoutEffect 的调用。 useLayoutEffect 的调用时机是 DOM 更新之后,浏览器未渲染之前,其调用要早于 useEffect ,其内部的更新计划为同步刷新。事实上, useLayoutEffect 与类组件中 componentDidMount 和 componentDidUpdate 调用时间是一致的,也都是同步刷新。相对而言,React 官网会推荐使用 useEffect 来替代 useLayoutEffect 。原因有三:两者执行时 DOM 都已经加载完毕,其中 useEffect 执行时,浏览器基本已经渲染完毕,不存在各种副作用执行的误差; useEffect 是经过调度器的回调在浏览器的空闲时机单独处理副作用的,其不会阻塞渲染(或者说不会提升渲染的执行成本)因此效率更高; useEffect 对 SSR 更加友好,不容易出现问题。参见 Hook API 索引:useLayoutEffect (opens new window)。
    2. 类组件生命周期 componentDidMount 或者 componentDidUpdate 的调用。类组件生命周期的调用都是同步的,因此生命周期的内容的执行实际上是会对渲染的过程具有一定的阻塞作用的。因此,对于组件中生命周期的设计而言,天然就具有这样的劣势,因为要想维护生命周期时机的正确性,必须要容忍其同步性。相对于言,副作用思想的组件设计就突破了这种劣势,因为副作用的执行是异步的、非阻塞式的,这也是函数式组件针对类组件的具有的优势之一。副作用天然就具有对执行时机的低耦合性,也就是说,在大部分场景下,我们所需要的副作用的场景并不需要阻塞渲染,因此,React 为我们提供了灵活的副作用的执行方式, useEffect 和 useLayoutEffect 的设计正是为此而生。
    3. 执行 Callback 副作用。使用 Callback 标记的各种 Callback 副作用本质上是批量同步执行的,包括类组件 setState 产生的回调副作用和 ReactDOM 中 render 函数或 hydrate 函数产生的回调副作用。

从整体上看,这三个函数除了对生命周期和副作用处理之外,其核心还是对照 EffectTag 以对 DOM 的各种操作进行提交,此部分内容将在下文详述。

# 参考

  • React 新旧生命周期的思考理解 (opens new window)
编辑 (opens new window)
上次更新: 2022/08/08, 19:51:35
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 ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式