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
      • 0.前言
      • 1.结构及作用
      • 2.workInprogress状态
      • 3. 提问 completeWork 流程
        • 问题:对下面 jsx 结构,分析 completeWork 递归逻辑?
        • 问题:请简述 completeWork 的核心逻辑?
        • 问题:对不同节点的逻辑处理包含哪两块逻辑?
        • 问题:描述下 completeWork 的 mount 阶段工作?
        • 问题:描述下 completeWrk 的 update 阶段工作?
      • 4. 提问 completUnitOfWork 流程
        • 问题:在 completeUnitOfWork 中是如何实现 completeWork 的深度优先遍历?
        • 问题:如何生成 EffectList 链表?
        • 问题:reconciler 递归结束
      • 5.额外说明
        • 1. 源码 ReactFiberHostConfig 为空函数?
        • 2. 待补充案例
    • React源码-commit前后阶段工作
    • React源码-commit核心
  • 百问掘金
  • 深入浅出 React
wangjiasheng
2023-07-06
目录

React源码-completeWork

# 0.前言

争取以提问的方式将 React 知识结构。

尽量精简代码,将无关逻辑省去。

# 1.结构及作用

本章节主要对应于 React 源码中 reconcile 阶段的 归 阶段。

其核心逻辑主要存在于两个函数中,completeWork 以及 completeUnitOfWOrk 中,两者的区别如下:

  1. 代码执行次序:completeUnitOfWork →\rightarrow→ completeWork
  2. 文件位置不同,completeUnitOfWork 位于 ReactFiberWorkLoop 中,而 completeWork 位于 ReactFiberComplteWork
  3. 功能不同:
    • completeWork 为核心处理逻辑,作用见后续详述。
    • completeUnitOfWork属于上层函数,主要是用于对 complteWork 回调的结果进行编排处理,如 EffectTag 链表的产生。

# 2.workInprogress状态

在 React 中无论代码逻辑怎么发生变化,实际上都是对 workInProgress 结构的修改,所以只需要把握住每个阶段,改变的是 workInProgress 上的什么属性,基本上能用一句话能说清 React 的更新逻辑。

在 completeWork 的阶段:

  1. mount 阶段:

    • workInProgress.stateNode :底层由 document.createElement 产生DOM 结构,并保存在此节点属性上。

    • 意义:fiber 双缓存的核心意义,一颗树用于显示页面,一颗树用于将动态计算的结果缓存,而 stateNode 的作用就是当 commit 阶段切换 current 指针时,可以立马将 DOM 结构渲染到页面上,而不存在空白中间态过程。

  2. update 阶段:

    • workInProgress.updateQueue 保存的是更新数组。结合案例具体结构如下:

      workInProgress.updateQueue = ["key1", "value1" , "key2", "value2"];
      
      1
    • workInProgress.firstEffect|nextEffect|lastEffect :保存 EffectList 链表。

# 3. 提问 completeWork 流程

# 问题:对下面 jsx 结构,分析 completeWork 递归逻辑?

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
      </header>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12

通过打断点,可知 complteWork 的次序如下:

  1. img :进入 HostComponent(5) 更新逻辑
  2. Edit :文本节点,进入 HostText (6)
  3. code:进入 HostComponent(5) 更新逻辑
  4. " and save to reload.":文本节点,进入 HostText (6)
  5. p :注意此时 p 对应的 workInProgress.child 存在,appendChild 只有在此刻才会生效。
  6. header:进入 HostComponent(5) 更新逻辑
  7. div:进入 HostComponent(5) 更新逻辑

为了帮助更好的理解,以问答的方式解答上述过程:

  1. 为什么第一个 completeWork 进入的 <img /> ?

    在 React 中children 的链表结构,当存在多个子节点的情况下,只会为第一个 children 生成 fiber 节点,其余节点均以 sibling 的方式与第一个 children 链接。

    又,header 存在两个子节点 img 和 p ,因此第一个 completeWork 会进入 <img />

  2. 为什么 Edit 要先于父节点 p ?

    因为 completeWork 是深度优先遍历,当子节点全部遍历结束后,才会遍历父节点。

  3. 为什么<code> 存在文本子节点不遍历?

    React 中对单文本子节点有做特殊优化,beginWork 阶段压根就没有这个 Fiber 节点。

# 问题:请简述 completeWork 的核心逻辑?

completWork 的核心逻辑如下:

根据 tag 类型,对不同的节点类型进行逻辑处理。

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
    switch (workInProgress.tag){
      case 组件1:
        /** 处理逻辑 */
        return null;
      case 组件2;
        /** 处理逻辑 */
    	  return null;
      case 组件3;
    		return null
      ....
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

因此在案例中:

  • 对于 div 或者 img 这类型结构,会进入case 为 HostComponent 的处理逻辑。

  • 对于 Eidt 或者 " and save to reload." 这类结构,会进入 case 为 HostText 的处理逻辑。

  • ......其余组件类似,就不再赘述。

需要注意的是在 completeWork 中有一大类组件是直接跳过处理的,因为在后续分析中可以发现 completeWork 中主要针对的是具有 DOM 结构的 fiber 节点,对于不会生成具体的 DOM 结构的节点,在 switch...case 阶段会直接跳过,代码如下:

switch (workInProgress.tag) {
  case IndeterminateComponent:
  case LazyComponent:
  case SimpleMemoComponent:
  case FunctionComponent:
  case ForwardRef:
  case Fragment:
  case Mode:
  case Profiler:
  case ContextConsumer:
  case MemoComponent:
    return null;
1
2
3
4
5
6
7
8
9
10
11
12
  1. IndeterminateComponent:IndeterminateComponent 是 React 内部使用的一个中间类型,用于表示还未确定具体类型的组件。它不会直接被应用开发者创建,而是在 React 的协调过程中根据组件的类型进行转化。

  2. LazyComponent:LazyComponent 是懒加载组件,通过 React.lazy() 函数来创建。React.lazy() 接受一个动态 import() 函数作为参数,用于异步加载组件的代码。在需要渲染该组件时,React 会自动按需加载该组件。

  3. SimpleMemoComponent:SimpleMemoComponent 是一个使用了浅比较的记忆组件(Memoized Component)。通过 React.memo() 函数来创建,它会将组件包裹起来并缓存组件的渲染结果。只有在组件接收的 props 发生变化时,才会触发重新渲染。

  4. FunctionComponent:FunctionComponent 是一种函数式组件,在 React 中最常见的组件类型。它由一个纯函数来定义,接收 props 作为输入并返回渲染的内容。可以使用函数组件来编写简单、可复用的 UI 组件。

  5. ForwardRef:ForwardRef 是一种用于转发 ref 的组件包装方式。通过 React.forwardRef() 函数来创建,可以将 ref 特性传递给子组件。这在某些情况下对于访问子组件上的 DOM 节点或实例非常有用。

  6. Fragment:Fragment 是一种不会输出 DOM 元素的容器组件。由 React.Fragment 或者简写形式 <>...</> 来创建。可以用它来返回多个子元素而无需包裹到额外的 DOM 节点中。

  7. Mode:Mode 是一个用于设置渲染模式的组件,用于控制 React 渲染的行为。通过 <React.StrictMode> 组件在应用根部使用,可以进行严格模式的检查和警告提示。

  8. Profiler:Profiler 是一个性能分析工具组件,用于度量和诊断 React 组件的渲染性能。通过 <Profiler> 组件来创建,并提供一个回调函数用于收集组件渲染过程的性能信息。

  9. ContextConsumer:ContextConsumer 是一个使用 React Context 的消费者组件。通过在组件中使用 useContext() 钩子或者在类组件中使用 <MyContext.Consumer> 组件来创建,用于接收和使用 Context 提供的值。

  10. MemoComponent:MemoComponent 是使用记忆化技术优化后的组件,通过 React.memo() 函数来创建。它会对组件的输入 props 进行浅比较,并只在 props 发生变化时才重新渲染。

# 问题:对不同节点的逻辑处理包含哪两块逻辑?

包含两部分逻辑:update 处理逻辑和 mount 处理逻辑。

举例: HostComponent 类型节点的处理逻辑,伪代码如下:

case HostComponent: {
  const isMount = current !== null && workInProgress.stateNode != null;
  if(!isMount){
    // 更新 HostComponent 逻辑
      updateComponent(
        current,
        workInProgress,
        type,
        newProps,
        rootContainerInstance,
      );
  }else{
    // 主要逻辑如下:【以 HostComponent 为主】
    // 1. `createInstance`:基于 `Fiber`  对象创建 `DOM` 结构。
    // 2. `appendAllChildren`:如何处理 `workInProgress.child` 节点
    // 3. `finalizeInitialChildren` :属性绑定。
  }
  // do something else......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

首先判断是 Update 阶段还是 Mount 阶段

  • 如果是 Update 阶段,则调用 UpdateComponent 函数,如对于 HostComponent 会调用 UpdateHostComponent;对于 HostText 会调用 UpdateHostText 函数。

  • 如果是 Mount 阶段,实际上会生成真实的 DOM 结构,并挂载在 workInProgress.stateNode 上。

对于多节点结构,需要处理下链表连接,代码逻辑:

case HostComponent: {
  const isMount = current !== null && workInProgress.stateNode != null;
  if(!isMount){
    // workInProgress.upadateQueue = ["title",2,"children",2] 
  }else{
    // workInProgress.stateNode = <div>...</div> (dom 结构)
  }
  node = (node: Fiber);
  if (node === workInProgress) {
    return;
  }
  
  /** 主要处理的存在节点存在子节点场景时,需要额外处理链表 */
  while (node.sibling === null) {
    if (node.return === null || node.return === workInProgress) {
      return;
    }
    node = node.return;
  }

  node.sibling.return = node.return;
  node = node.sibling;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

主要针对如下 JSX 结构:

<p>
  Edit <code>src/App.js</code> and save to reload.
</p>
1
2
3
  • 若此时 node 为 Edit 节点,node.sibling 为 <code /> 节点。

    // node = "Edit"
    // node.sibling = <code />
    // node.return = <p>
    // 结果:<code />.return = <p>;
    node.sibling.return = node.return;
    // node = code 继续找兄弟
    node = node.sibling;
    
    1
    2
    3
    4
    5
    6
    7
  • 结果为:所有子节点的 return 均执行父节点 <p>

# 问题:描述下 completeWork 的 mount 阶段工作?

为 HostComponent 生成 DOM 结构,并缓存在 workInProgress.stateNode 结构中。

依次调用如下:

  1. createInstance:基于 Fiber 对象创建 DOM 结构。
  2. appendAllChildren:如何处理 workInProgress.child 节点
  3. finalizeInitialChildren :属性绑定。

createInstance

本质调用 window.createElement

此函数还不好找,在 ReactDOMHostConfig.js 中,此函数不同宿主环境中还不同,以下示例 web 环境:

export function createInstance(
  type: string, /* workInProgress.type */
  props: Props, /* workInProgress.pendingProps */
  rootContainerInstance: Container, /* 根节点 */
  hostContext: HostContext, /* 请无视这个属性 */
  internalInstanceHandle: Object, /* workInProgress */
): Instance {
  let parentNamespace: string;
  // DOM 原生的 `createElement`
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  return domElement;
}


var randomKey = Math.random().toString(36).slice(2);
// 辅助函数:precacheFiberNode: 往 DOM 帮上 fiber 结构
function precacheFiberNode(hostInst, domElement) {
  domElement[__reactFiber$' + randomKey] = hostInst;
}
// 辅助函数:updateFiberProps: 往 DOM 帮上 props 属性
function updateFiberProps(domElement, props) {
  domElement['__reactProps$' + randomKey] = props;
}
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

appendAllChildren

功能:将子节点的 dom 全部挂载在父节点 dom 下

举例:

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
      </header>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12

complteWork 的流程:

  • img :进入 HostComponent(5) 更新逻辑

  • Edit :文本节点,进入 HostText (6)

  • code:进入 HostComponent(5) 更新逻辑

  • " and save to reload.":文本节点,进入 HostText (6)

  • p :注意此时 p 对应的 workInProgress.child 存在,appendChild 只有在此刻才会生效。

注:workInProgress.type === "p" 才会进入逻辑:

appendAllChildren = function (
    parent: Instance,
    workInProgress: Fiber,
    needsVisibilityToggle: boolean,
    isHidden: boolean,
  ) {
    let node = workInProgress.child;
    while (node !== null) {
      if (node.tag === HostComponent) {
         /** img 标签或 code 标签会进入此逻辑*/
        let instance = node.stateNode;
        if (needsVisibilityToggle && isHidden) {
          // This child is inside a timed out tree. Hide it.
          const props = node.memoizedProps;
          const type = node.type;
          instance = cloneHiddenInstance(instance, type, props, node);
        }
        appendInitialChild(parent, instance);
      } else if (node.tag === HostText) {
        /** Edit 标签或 and save to reload. 标签会进入此逻辑*/
        let instance = node.stateNode;
        if (needsVisibilityToggle && isHidden) {
          // This child is inside a timed out tree. Hide it.
          const text = node.memoizedProps;
          instance = cloneHiddenTextInstance(instance, text, node);
        }
        appendInitialChild(parent, instance);
      }
    
     /* 省略其余场景代码 */

      node = (node: Fiber);
      if (node === workInProgress) {
        return;
      }
      while (node.sibling === null) {
        if (node.return === null || node.return === workInProgress) {
          return;
        }
        node = node.return;
      }
      
      // 将所有 children 的return 值都指向父节点。
      node.sibling.return = node.return;
      node = node.sibling;
    }
  };
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

核心逻辑:appendInitialChild(parent, instance)

步骤:

  1. p 标签进入 completeWork 逻辑,此时其子 children 均已完成 completeWork 流程。
  2. appendAllChildren 会循环将子children 上的 dom 结构,以及通过 appendChild 挂载到 p 上。
  3. 所有子节点的 return 值均指向 p
  4. 最终结果:p 标签拥有完整的 dom 结构。

finalizeInitialChildren

此函数还不好找,在 ReactDOMHostConfig.js 中,此函数不同宿主环境中还不同,以下示例 web 环境:

export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
  setInitialProperties(domElement, type, props, rootContainerInstance);
  return shouldAutoFocusHostComponent(type, props);
}
1
2
3
4
5
6
7
8
9
10

此函数主要用途,给 DOM 结构挂载属性。

比如此结构:

<img src={logo} className="App-logo" alt="logo" />
1
  1. 判断 <img /> 是否为 CustomComponent

  2. React 中需要对 DOM 的原生事件进行监听,核心函数 listenToNonDelegatedEvent

    switch (tag) {
        case 'dialog':
          listenToNonDelegatedEvent('cancel', domElement);
          listenToNonDelegatedEvent('close', domElement);
          props = rawProps;
          break;
    
        case 'iframe':
        case 'object':
        case 'embed':
          // We listen to this event in case to ensure emulated bubble
          // listeners still fire for the load event.
          listenToNonDelegatedEvent('load', domElement);
          props = rawProps;
          break;
    
        case 'video':
        case 'audio':
          // We listen to these events in case to ensure emulated bubble
          // listeners still fire for all the media events.
          for (var i = 0; i < mediaEventTypes.length; i++) {
            listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
          }
    
          props = rawProps;
          break;
    
        case 'source':
          // We listen to this event in case to ensure emulated bubble
          // listeners still fire for the error event.
          listenToNonDelegatedEvent('error', domElement);
          props = rawProps;
          break;
    
        case 'img':
        case 'image':
        case 'link':
          // We listen to these events in case to ensure emulated bubble
          // listeners still fire for error and load events.
          listenToNonDelegatedEvent('error', domElement);
          listenToNonDelegatedEvent('load', domElement);
          props = rawProps;
          break;
    
        case 'details':
          // We listen to this event in case to ensure emulated bubble
          // listeners still fire for the toggle event.
          listenToNonDelegatedEvent('toggle', domElement);
          props = rawProps;
          break;
    
        case 'input':
          initWrapperState(domElement, rawProps);
          props = getHostProps(domElement, rawProps); // We listen to this event in case to ensure emulated bubble
          // listeners still fire for the invalid event.
    
          listenToNonDelegatedEvent('invalid', domElement);
          break;
    
        case 'option':
          validateProps(domElement, rawProps);
          props = rawProps;
          break;
    
        case 'select':
          initWrapperState$1(domElement, rawProps);
          props = getHostProps$1(domElement, rawProps); // We listen to this event in case to ensure emulated bubble
          // listeners still fire for the invalid event.
    
          listenToNonDelegatedEvent('invalid', domElement);
          break;
    
        case 'textarea':
          initWrapperState$2(domElement, rawProps);
          props = getHostProps$2(domElement, rawProps); // We listen to this event in case to ensure emulated bubble
          // listeners still fire for the invalid event.
    
          listenToNonDelegatedEvent('invalid', domElement);
          break;
    
        default:
          props = rawProps;
      }
    
    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
  3. 调用 setInitialDOMProperties 函数,将 jsx 中获取的 props 绑定给 node

    会循环遍历 nextProps 值,最终会调用 node.setAttribute(attributeName, attributeValue) 函数。

    其中,对于单次循环,执行前:node 为 <img />

    • attributeName: src
    • attributeValue: "/static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg"

    执行后 node 为 <img src="/static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg">

    注意的是,在执行 node.setAttribute 函数时,DOM 结构也是存在很多的保留字段,如下:

    const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';
    const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';
    const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
    const AUTOFOCUS = 'autoFocus';
    const CHILDREN = 'children';
    const STYLE = 'style';
    const HTML = '__html';
    
    1
    2
    3
    4
    5
    6
    7

    具体的逻辑在 setInitialDOMProperties 函数中,伪代码如下:

    function setInitialDOMProperties(
      tag: string,
      domElement: Element,
      rootContainerElement: Element | Document,
      nextProps: Object,
      isCustomComponentTag: boolean,
    ): void {
      for (const propKey in nextProps) {
        if(propKey === "style"){
          // 执行 setValueForStyles() 函数
        }else if(propKey === "children"){
          // 执行 setTextConent() 函数
        }else if(propKey === "dangerouslySetInnerHTML"){
          // 执行 setInnerHTML() 函数
        }else if(propKey === "suppressHydrationWarning" || 'suppressContentEditableWarning'){
          // Noop 
        }else if(propKey === "autoFocus"){
          // React 会对此参数在 commit 阶段进行特殊处理,
        }else{
          // 调用 setValueForProperty()
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    我们刚才 node.setAttribute() 是在 setValueForProperty 中处理的。

# 问题:描述下 completeWrk 的 update 阶段工作?

为了能够简化更新流程,只保留更新阶段所需最简洁的 JSX 结构:

import { useState } from "react";

function App() {
  const [num, setNum] = useState(1);
  return (
    <div className="App">
      <header className="App-header">
        <p onClick={() => setNum(x => x + 1)}>
          <code title={num}>{num}</code>
        </p>
      </header>
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在上述代码中,存在副作用的节点有两个 <p /> 和 <code />。

其中 p 标签也会标记为副作用的原因,每次更新时箭头函数的引用地址也发生改变。

操作:在状态更新前, num 值为 1 ,点击 p 标签后,num 被修改为 2。

最终结果:

workInProgress.updateQueue = ["title", 2, "children", "2"];
1

伪代码如下:

// 获取旧属性 {"title": 1 , "children": 1}
const oldProps = current.memoizedProps 

// 获取新属性 {"title": 2 , "children": 2}
const newProps = workInProgress.pendingProps;

// 获取 DOM 结构
var instance = workInProgress.stateNode;

// 需要比较新旧节点 【diffProperties】
// 注:此部分工作牵涉 DOM 结构的比对,实际是由 `react-dom` 实现
workInProgress.updateQueue = diffProperties(instance,oldProps,nextProps) 

// 打 EffcectTag
if(updatePayload){
  workInProgress.flags |= Update;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

看下 react-dom 中是如何实现 props diff 比对的?

相比上直接对比两个对象结构的比对, DOM 上的 props 的 diff 操作需要额外的考虑更多:

  1. 不同的 dom 结构,不同的 tag 具有不同的 attributeName 。

    如:<img /> 上 src 属性,

  2. 对于特殊 props 的处理,如 style 的变化逻辑,会专门标注出来。

    const DANGEROUSLY_SET_INNER_HTML = 'dangerouslySetInnerHTML';
    const SUPPRESS_CONTENT_EDITABLE_WARNING = 'suppressContentEditableWarning';
    const SUPPRESS_HYDRATION_WARNING = 'suppressHydrationWarning';
    const AUTOFOCUS = 'autoFocus';
    const CHILDREN = 'children';
    const STYLE = 'style';
    const HTML = '__html';
    
    1
    2
    3
    4
    5
    6
    7
  3. 判断变化属性的性质

    删除: {"title": 1 , "children": 1} →\rightarrow→ {"childre": 1}

    新增:{"children":1} →\rightarrow→ {"title": 1 , "children": 1}

    更新: {"title": 1 , "children": 1} →\rightarrow→ {"title": 2 , "children": "2"}

伪代码:

function diffProperties(instance,oldProps, nextProps){
  for(propKey in oldProps){
    if(
      nextProps.hasOwnProperty(propKey) // nextProps 存在
      || !oldProps.hasOwnProperty(propKey) // oldProps 不存在
      || oldProps[propKey] == null // oldProps 存在但为 null 值
    ){
      continue;
    }
    // 将删除逻辑 push 到 updateQueue 数组中 {"title", null} 
    (updatePayload = updatePayload || []).push(propKey, null);
  }
  
  for (propKey in nextProps) {
    // 从 oldProps 和 nextProps 取出属性值
    const nextProp = nextProps[propKey];
    const lastProp = lastProps != null ? lastProps[propKey] : undefined;
    if (
      !nextProps.hasOwnProperty(propKey)  // nextProps 不存在
      || nextProp === lastProp  // nextProp 和 oldProps 相同 
      || (nextProp == null && lastProp == null) // 都为null
    ) {
      continue;
    }
    
    // 对于 DOM 元素上各种保护字段的处理
    
    // 将新增或更新逻辑 push 到 updateQueue 数组中
    (updatePayload = updatePayload || []).push(propKey, nextProp);
  }
  
  // 将 style 的变化 push 到 updateQueue 数组中
  if (styleUpdates) {
    (updatePayload = updatePayload || []).push("style", styleUpdates);
  }
}
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

# 4. 提问 completUnitOfWork 流程

# 问题:在 completeUnitOfWork 中是如何实现 completeWork 的深度优先遍历?

深度遍历的逻辑,是由 ReactFiberWorkLoop 文件中编写的。

在协调器的入口函数中:

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// 调度:beginWork → completeWork 
function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  const next = beginWork(current, unitOfWork);
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

如果忽略掉 performUnitOfWork 这个函数,workLoopSync会循环检查 workInProgress 是否有值,即隐藏一个条件:workInProgress 会跳出循环。

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do{
    const current = completedWork.alternate;
    const next = completeWork(current, completedWork);
    
    // 注解1:跳出 completeWork 循环
    if (next !== null) {
      workInProgress = next;
      return;
    }
    
    const siblingFiber = completedWork.sibling;
    
    // 注解2:当存在兄弟节点时,也会跳出循环。
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    
    // 省略生成 EffectList 链表,见下
   
    // 将链表移动到父节点
    workInProgress = completedWork.return;
  } while (completedWork !== null);
}
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
  • 注解1:completeWork 处理

通过看源码中 completeWork 的回调值,正常返回 null ,只有少数几个 case 会返回 Fiber 结构,错误处理或 SuspendCompnent 处理。

case SuspenseComponent:
 ....
 if ((workInProgress.flags & DidCapture) !== NoFlags) {
   // Something suspended. Re-render with the fallback children.
   workInProgress.lanes = renderLanes;
   return workInProgress;
 }

case SuspenseListComponent:
  ....
  return workInProgress.child;
1
2
3
4
5
6
7
8
9
10
11
  • 注解2:主要考虑对于以下 jsx 结构
<div>
 <p>
    Edit <code>src/App.js</code> and save to reload.
 </p>
</div>
1
2
3
4
5

当进入 Edit 这个文本节点时,存在其兄弟节点(<code />) ,此时应该结束 Edit 的 completeWork 遍历,并进入 <code /> 节点的 beginWork 流程,即生成 <code /> 这个 DOM 元素,直到遍历完所有 p 的子节点,最后执行 p 节点的 completeWork。

# 问题:如何生成 EffectList 链表?

此部分逻辑,也是在 completeUnitOfWork 中实现的。

链表逻辑如下:

const returnFiber = completedWork.return;
if (
  returnFiber !== null &&
  (returnFiber.flags & Incomplete) === NoFlags
) {
  if (returnFiber.firstEffect === null) {
    returnFiber.firstEffect = completedWork.firstEffect;
  }
  if (completedWork.lastEffect !== null) {
    if (returnFiber.lastEffect !== null) {
      returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
    }
    returnFiber.lastEffect = completedWork.lastEffect;
  }

	if (returnFiber.lastEffect !== null) {
      returnFiber.lastEffect.nextEffect = completedWork;
    } else {
      returnFiber.firstEffect = completedWork;
    }
  returnFiber.lastEffect = completedWork;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

上面的代码实在过于抽象,结合 jsx 结构来分析:

function App() {
  const [num, setNum] = useState(1);
  return (
    <div className="App">
      <header className="App-header">
        <p onClick={() => setNum(x => x + 1)}>
          <code title={num}>{num}</code>
        </p>
      </header>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12

第一个进入 completeUnitOfWork 的节点为 <code /> ,注,

进入第一个具有副作用的节点 <code /> ,对其父节点 <p /> 做如下操作:

  • p.firstEffect = <code />
  • p.lastEffect = <code />

向上遍历,进入 p 节点 ,对其父节点 <header /> 做如下操作:

  • header.firstEffect = <code />
  • <p />.nextEffect = <code />
  • header.lastEffect = <p />

ok,后续不再分析了,简单来说,EffectList 形成的是一条链表,任意一个 Fiber 节点,通过 firstEffect 可以找到第一个需要更新的节点,通过 nextEffect 找到下一个需要更新的节点。

在上述 jsx 中只存在两个副作用节点 <code> | <p> 节点,因此无论是 workInProgress 指向的是 <div /> 或者 <headers /> 其副作用链表均为:

workInProgress(div|header) 
→ workInProgress.firstEffect(code) 
→ workInProgress.firstEffect.nextEffect(p)
1
2
3

下一个问题,通过打印入口也是类似的结果。

# 问题:reconciler 递归结束

核心代码:

// 调度: render 阶段 => commit 阶段
function performConcurrentWorkOnRoot(root) {
  // 省略若干代码.....
  var finishedWork = root.current.alternate;
  root.finishedWork = finishedWork;
  // commit 阶段入口
  commitRoot(root);
}
1
2
3
4
5
6
7
8

通过在终端打印如下:

> finishedWork
FiberNode {tag: 3, key: null, elementType: null, type: null, stateNode: FiberRootNode, …}
> finishedWork.firstEffect
FiberNode {tag: 5, key: null, elementType: 'code', type: 'code', stateNode: code, …}
> finishedWork.firstEffect.updateQueue
(4) ['title', 2, 'children', '2']
> finishedWork.firstEffect.nextEffect
FiberNode {tag: 5, key: null, elementType: 'p', type: 'p', stateNode: p, …}
> finishedWork.firstEffect.nextEffect.nextEffect
null
1
2
3
4
5
6
7
8
9
10

进入 commmit 阶段的实际上是 root 根节点(tag 一定为 3 )

  • 通过 firstEffect 可以找到第一个需要更新的节点,并且通过 updateQueue 中保存了对应需要改变的 props 属性(此部分由 domDifPropties 函数比较产生)

  • 通过 nextEffect 可以继续查找节点,直至最后一个节点 null

# 5.额外说明

# 1. 源码 ReactFiberHostConfig 为空函数?

在 ReactFiberCompletWork.old.js 中的开头一部分比较疑惑,点击进去,发现此文件为一个空函数。

import {
  createInstance,
  createTextInstance,
  appendInitialChild,
  finalizeInitialChildren,
  prepareUpdate,
  supportsMutation,
  supportsPersistence,
  cloneInstance,
  cloneHiddenInstance,
  cloneHiddenTextInstance,
  createContainerChildSet,
  appendChildToContainerChildSet,
  finalizeContainerChildren,
  getFundamentalComponentInstance,
  mountFundamentalComponent,
  cloneFundamentalInstance,
  shouldUpdateFundamentalComponent,
  preparePortalMount,
  prepareScopeUpdate,
} from './ReactFiberHostConfig';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

此 ReactFiberHostConfig.js 在运行时会被定位到 ReactDOMHostConfig.js 文件,这是 react 在架构上单独将跨端抽离出来了。

因此可根据不同的宿主环境进行切换的,比如说在浏览器环境下,该文件路径实际解析执行的是 ReactDOM 文件下的 ReactDOMHostConfig.js 文件。

这也是在 React 中将 div 或 img 对标的处理组件命名为 HostComponent 的原因。

Host 在编程语言里头感觉上可以翻译为 宿主环境,如在 web 环境下,HostComponent 可以被认为值得是 div 或 img 这类标签;而在 Native 下 HostComponent 对标的是类似于 View 这类基础组件。

# 2. 待补充案例

style 属性的变化
属性更新的变化
属性删除的变化
1
2
3
编辑 (opens new window)
上次更新: 2023/09/17, 22:09:00
React源码-ReactFiberWorkLoop.js
React源码-commit前后阶段工作

← React源码-ReactFiberWorkLoop.js React源码-commit前后阶段工作→

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