Jacky's blog Jacky's blog
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Jacky Wang

行到水穷处,坐看云起时
首页
  • 编码专题
  • 深入浅出 Vite
  • 深入浅出 babel
  • 快速上手API
  • 深入浅出 react
  • Node

    • code-notebook
  • 状态管理

    • redux
  • 前端工程化

    • Wepack
  • React源码

    • React源码
  • 组件库封装

    • 组件库
  • 开发工具

    • Vscode 插件
  • 项目展示
  • 案例中心 (opens new window)
  • First Project
  • 基础算法题
  • 链表题
  • 动态规划
  • 双指针
  • 递归
  • 数据结构
  • 前端学习计划 (opens new window)
  • 技术随笔
  • 转载文章
  • 包管理工具
  • 前端学习周报
  • VSCode插件
  • Promise 专题
  • 函数技巧
  • React 专题
  • 配置文件

    • TSCONFIG-配置 (opens new window)
    • NGINX-配置 (opens new window)
    • 正则规则查询手册 (opens new window)
    • Lint 配置 (opens new window)
  • 教程

    • GIT-教程
    • NPM SCRIPTS-工作流 (opens new window)
    • DOCKER-教程 (opens new window)
    • LERNA-教程 (opens new window)
    • GIT-常用操作整理 (opens new window)
  • VSCode

    • LAUNCH.JSON (opens new window)
  • 指令

    • NPM 指令 (opens new window)
    • NVM 指令 (opens new window)
    • Nginx 指令 (opens new window)
    • YARN 指令 (opens new window)
    • PNPM 指令 (opens new window)
  • 库

    • FS-EXTRA 库 (opens new window)
    • NODE 库-PATH (opens new window)
  • 永远的神

    • 魔法师卡颂-自顶向下学 React 源码 (opens new window)
    • 全栈潇晨 (opens new window)
    • 博客-程序员山月-Daily (opens new window)
    • 淘系前端:冴羽 (opens new window)
  • 系列文章

    • 《图解HTTP》 (opens new window)
    • 《ES6标准入门》 (opens new window)
    • 《现代JavaScript教程》 (opens new window)
    • 《深入浅出Webpack》 (opens new window)
    • VSCode 插件系列:小茗同学 (opens new window)
    • JEST 教程 (opens new window)
    • 前端精读周刊:各种精读系列 (opens new window)
    • 一文吃透系列 (opens new window)
    • 图解 REACT 原理 (opens new window)
  • 实用网站

    • MDN (opens new window)
    • CAN I USE (opens new window)
    • TYPESCRIPT-ESLint-RULES (opens new window)
    • ESLint-RULES (opens new window)
    • FRONT-END TREND (opens new window)
    • NPM TREND (opens new window)
    • 在线分析 Node 依赖 (opens new window)
    • FIND NPM (opens new window)
    • CODE PEN (opens new window)
    • 印记中文 (opens new window)
    • TOOL.LU (opens new window)
    • 阮一峰-网道 (opens new window)
    • DIGITAL OCEAN (opens new window)
    • DEVDOCS.IO (opens new window)
    • JOI (opens new window)
  • 算法

    • 小浩算法 (opens new window)
    • LABULADONG 的算法小抄 (opens new window)
    • 力扣 SOLUTION (opens new window)
    • HACKER RANK (opens new window)
    • 代码随想录 (opens new window)
  • 博客系列

    • 美团大佬 (opens new window)
    • 蜡笔小伟 (opens new window)
    • 优秀博客1 (opens new window)
    • 优秀博客2-umi (opens new window)
    • 优质博客 (opens new window)
  • CSS

    • CSS-EASING 库 (opens new window)
    • ROUGH.JS (opens new window)
    • CSS 网站收集
    • UNOCSS (opens new window)
  • 前端

    • PROMISE (opens new window)
    • UNDERSCORE.JS (opens new window)
    • study with BGM (opens new window)
    • nginx【B站视频】 (opens new window)
    • 机器学习
    • Js基础
  • 掘金已购课程

    • 前端自动化测试精讲 (opens new window)
    • 深入浅出 Vite (opens new window)
    • 现代 Web 布局 (opens new window)
    • 前端算法与数据结构 (opens new window)
    • 基于 Vite 的 SSG 框架开发实战 (opens new window)
    • SSR 实战:官网开发指南 (opens new window)
    • WebGL 入门与实践 (opens new window)
    • 玩转 CSS 的艺术之美 (opens new window)
    • 前端调试通关秘籍 (opens new window)
    • React 进阶实践指南 (opens new window)
    • TypeScript 全面进阶指南 (opens new window)
    • 前端缓存技术与方案解析 (opens new window)
    • npm scripts 前端工作流 (opens new window)
    • Webpack5 核心原理与应用实践 (opens new window)
  • 购物车

    • 张鑫旭-技术写作指南 (opens new window)
    • 深入剖析 Node.js 底层原理 (opens new window)
    • 前端开发者的现代 C++ 课 (opens new window)
    • 从前端到全栈 (opens new window)
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 编码专题

  • 深入浅出 Vite

  • 快速上手 API

  • 深入浅出Babel

  • 深入浅出 React

    • React源码-常量定义
    • React源码-WorkInProgress常用操作
    • React源码-目录结构
    • React源码-workInProgress详解
    • React源码-ReactFiberWorkLoop.js
    • React源码-completeWork
    • React源码-commit前后阶段工作
    • React源码-commit核心
      • 0. 前言
      • 1. 阶段
      • 2. BeforeMutation
        • 2.1 处理 Blur 事件(省略)
        • 2.2 处理 Class 组件的生命周期
        • 2.3 处理 function 组件上的 useEffect
      • 3. Mutation 阶段
        • 3.1 重置文本节点
        • 3.2 处理 ref 属性
        • 3.3 如何处理插入 DOM 元素
      • 备注
  • 百问掘金
  • 深入浅出 React
wangjiasheng
2023-07-15
目录

React源码-commit核心

# 0. 前言

此部分是承接上部分 commit 阶段前后工作的。

单拎出来分析的原因,before mutation 、mutation 和 layout 其中用到的函数在 react-recomciler/ReactFiberCommitWOrk.js 文件中。

# 1. 阶段

Renderer工作的阶段被称为commit阶段。commit阶段可以分为三个子阶段:

  • before mutation 阶段(执行DOM操作前)
  • mutation 阶段(执行DOM操作)
  • layout 阶段(执行DOM操作后)

这三个阶段的函数有个共性的点,就是会循环遍历 effectList 链表,伪代码如下:

const nextEffect = effectList.firstEffect;
do {
  // effectList 会被遍历一遍,三个阶段就会被遍历三遍
  nextEffect = nextEffect.nextEffect;
}while(nextEffect !== null);
1
2
3
4
5

接下来我们来对每个阶段进行具体的分析。

# 2. BeforeMutation

第一个 do...while 语句,伪代码如下:

do {
  try {
    commitBeforeMutationEffects();
  } catch (error) {
    invariant(nextEffect !== null, "Should be working on an effect.");
    captureCommitPhaseError(nextEffect, error);
    // 报错后,继续调度执行
    nextEffect = nextEffect.nextEffect;
  }
} while (nextEffect !== null);
1
2
3
4
5
6
7
8
9
10

核心处理逻辑在 commitBeforeMutationEffects

function commitBeforeMutationEffects() {
  while (nextEffect !== null) {
    const current = nextEffect.alternate;

    // 1. 处理`DOM节点`渲染/删除后的 `autoFocus`、`blur`逻辑【省略】

    // 2. 调用`getSnapshotBeforeUpdate`生命周期钩子

    // 3. 调度`useEffect`

    // 持续读取 nextEffect 链表
    nextEffect = nextEffect.nextEffect;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

递归遍历的逻辑:nextEffect = nextEffect.nextEffect;

# 2.1 处理 Blur 事件(省略)

这部分没啥意义,可以跳过。

# 2.2 处理 Class 组件的生命周期

预先知识复习:

  • getSnapshotBeforeUpdate(prevProps,preState)方法在最近一次渲染输出(提交到 DOM 节点)之前调用。

    用过代码就能理解,此时获取的值为 current 树上的 props 和 state 值。

    const prevProps = current.memoizedProps;
    const prevState = current.memoizedState;
    
    1
    2

    这里current 树为当前页面展示的树,最新的值是保存在 workInProgress 中的,这也就是取名为啥叫 prevState 和 prevProps 的原因

  • getSnapshotBeforeUpdate() 方法需要与 componentDidUpdate() 方法一起使用,否则会出现错误。

    生命周期执行次序:getSnapshotBeforeUpdate → componentDidUpdate

    getSnapshotBeforeUpdate(prevProps,preState){
      console.log("#enter getSnapShotBeforeUpdate");
      return null;
    }
    
    // snapshot 为上述周期的返回值
    componentDidUpdate(prevProps, prevState, snapshot){
      console.log("#enter componentDidUpdate snapshot = ", snapshot);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

判断是否具有 Snapshot 副作用,若存在则调用 commitBeforeMutationEffectOnFiber()

const flags = nextEffect.flags;
if ((flags & Snapshot) !== NoFlags) {
  commitBeforeMutationEffectOnFiber(current, nextEffect);
}
1
2
3
4

具体针对的是 Class Component 组件:

function commitBeforeMutationLifeCycles(
  current: Fiber | null,
  finishedWork: Fiber,
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block: {
      return;
    }
    case ClassComponent: {
      if (finishedWork.flags & Snapshot) {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          // 执行 getSnapshotBeforeUpdate(preProps,preState);
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
      }
      return;
    }
  }
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

# 2.3 处理 function 组件上的 useEffect

// 判断当前函数是否使用 useEffect
if ((flags & Passive) !== NoFlags) {
  // 当 root 上没有标识时,修改全局变量为 true
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    // 使用 schedule 包对 useEffect 内容进行调度【该包可以独立拆分使用】
    scheduleCallback(NormalSchedulerPriority, () => {
      // 触发useEffect
      flushPassiveEffects();
      return null;
    });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 3. Mutation 阶段

这部分是核心中的核心:如何操作 DOM 元素,内部核心调用底层的 DOM API 对页面进行视图更新。

首先依旧是 do...while() 循环,其中涉及到报错处理:

nextEffect = firstEffect;
do {
  try {
    commitMutationEffects(root, renderPriorityLevel);
  } catch (error) {
    invariant(nextEffect !== null, "Should be working on an effect.");
    captureCommitPhaseError(nextEffect, error);
    nextEffect = nextEffect.nextEffect;
  }
} while (nextEffect !== null);
1
2
3
4
5
6
7
8
9
10

核心处理逻辑在 commitBeforeMutationEffects

function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
  // 遍历 EffectList 链表
  while (nextEffect !== null) {
    const current = nextEffect.alternate;

    // 1. 根据 ContentReset effectTag重置文字节点

    // 2. 更新ref

    // 3. 根据 effectTag 分别处理
    const primaryEffectTag =
      effectTag & (Placement | Update | Deletion | Hydrating);
    switch (primaryEffectTag) {
      case Placement: {
        nextEffect.effectTag &= ~Placement;
        break;
      }
      case PlacementAndUpdate: {
        nextEffect.effectTag &= ~Placement;
        break;
      }
      case Hydrating: {
        nextEffect.effectTag &= ~Hydrating;
        break;
      }
      case HydratingAndUpdate: {
        nextEffect.effectTag &= ~Hydrating;
        break;
      }
      case Update: {
      }
      case Delection: {
      }
    }

    // 持续读取 nextEffect 链表
    nextEffect = nextEffect.nextEffect;
  }
}
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

递归遍历的逻辑:nextEffect = nextEffect.nextEffect;

# 3.1 重置文本节点

const flags = nextEffect.flags;

if (flags & ContentReset) {
  commitResetTextContent(nextEffect);
}
1
2
3
4
5

# 3.2 处理 ref 属性

暂时不纠结这块内容

# 3.3 如何处理插入 DOM 元素

当 EffectTag 为 Placement 时,触发 commitPlacement(nextEffect) 函数。

!注意:此函数来源于 ReactFiberCommitWork.js 文件中。

这边的难点在于 DOM 结构与 Fiber 结构不一致,导致过程相当复杂,举例如下:

想要在 App 的 <div /> 内插入 <p /> 结构:

function Item() {
  return <li></li>;
}

function App() {
  return (
    <div>
      + <p></p>
      <Item/>
    </div>
  )
}

// DOM树(旧)
#root ---> div ---> li

// DOM树(新)
#root ---> div ---> p
             |
               ---> li
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

虽然只插入一个节点,但是对于 Fiber 结构确是个大改动。

// Fiber树 - 改动前:
          child         child      child          child
rootFiber -----> App[×] -----> div -----> Item[×] -----> li


// Fiber树 - 改动后:
          child         child      child
rootFiber -----> App[×] -----> div -----> p
                                       | sibling[+]       child
                                       | -------> Item[x] -----> li
1
2
3
4
5
6
7
8
9
10

EffectTag 为 Placement 状态分析:

function Item() {
  const [isShow, setIsShow] = useState(false);
  return (
    <>
      {isShow && <p>+p Item</p>}
      <li onClick={() => setIsShow(true)}>Item</li>
    </>
  );
}

function App() {
  /* const [isShow, setIsShow] = useState(false); */
  return (
    <div>
      {/* {isShow && <p>+p</p>} */}
      <Item />
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

根据断点分析如下:

> firstEffect
FiberNode {tag: 5, key: null, elementType: 'p', type: 'p', stateNode: p, …}
> firstEffect.nextEffect
FiberNode {tag: 5, key: null, elementType: 'li', type: 'li', stateNode: li, …}
1
2
3
4

对于以上情况来说,在更新阶段,为 <p /> 下一个更新节点为兄弟节点 <li />

若 JSX 为如下:

function Item() {
  return (
    <>
      <li>Item</li>
    </>
  );
}

function App() {
  const [isShow, setIsShow] = useState(false);
  return (
    <div onClick={() => setIsShow(true)}>
      {isShow && <p>+p</p>}
      <Item />
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

根据断点分析 update 阶段生成的 EffectList 链表如下:

> firstEffect
FiberNode {tag: 5, key: null, elementType: 'p', type: 'p', stateNode: p, …}
> firstEffect.nextEffect
FiberNode {tag: 5, key: null, elementType: 'div', type: 'div', stateNode: div, …}
1
2
3
4

第一个节点为 <p /> 标签,以下一个节点为父节点 <div />

接下来逻辑如下:

  • 在 ReactFiberWorkLoop 文件中:

    function commitMutationEffects(
    	switch (primaryFlags) {
          case Placement: {
            commitPlacement(nextEffect/*EffectList 链表*/);
          }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
  • 在 ReactFiberCommitWork.js 文件中的 commitPlacement 函数:

    核心是对以下三个变量的处理:parentDOM 、siblingDOM 、selfDOM

    function commitPlacement(finishedWork: Fiber): void {
      const parentFiber = getHostParentFiber(finishedWork);
      // 找到并取出具有 DOM 结构的父组件
      const parentInstance = parentFiber.stateNode;
      // 当前 EffectList 自身是否具有 DOM 结构
      const isHost = tag === HostComponent || tag === HostText;
      // 当前 NextEffect 的 DOM 结构
      const childInstance = isHost ? node.stateNode : node.stateNode;
      // 找到 NextEffect 的兄弟组件
      const beforeInstance = getHostSibling(finishedWork);
      if (before) {
        parentStateNode.insertBefore(childInstance, beforeInstance);
      } else {
        parentStateNode.appendChild(childInstance);
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    PS:以上代码和源码出入较大,但是核心逻辑就是如此。

# 备注

  1. scheduleCallback 与 runWithPriority 有什么区别?

  2. flushPassiveEffect 中存了什么?

    flushSyncCallbackQueue 的区别?

编辑 (opens new window)
上次更新: 2023/09/17, 22:09:00
React源码-commit前后阶段工作

← React源码-commit前后阶段工作

最近更新
01
如何理解浏览器的 user agent 用户代理的含义?
11-05
02
浏览器事件循环机制
10-31
03
浏览器页面渲染机制【2023】
10-15
更多文章>
Theme by Vdoing | Copyright © 2020-2023
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式