[FE] React 初窥门径(四):React 组件的加载过程(render 阶段)

1. 回顾

前几篇文章中,我们采用了 VSCode 插件 CodeTour 来记录代码的执行过程,
并把相关的数据 .tour/ 放到了 github: thzt/react-tour 中。

截止到本文为之,我们总共记录了这些 code-tour,

.tour/
├── 2. 构建过程.tour
├── 3.1 react 的加载过程.tour
├── 3.2 react-dom 的加载过程.tour
├── 4.1 组件加载过程:函数组件(call stack).tour
├── 4.1.1 组件加载过程:函数组件(全流程).tour
└── 4.2 组件加载过程:类组件(call stack).tour

本文重点介绍 4.1.1 组件加载过程:函数组件(全流程) 相关的内容。

2. 极简的示例项目

现在我们开始介绍 React 函数组件的加载全流程,我们的示例项目如下,
github: thzt/react-tour/example-project

example-project/
├── README.md
├── package.json
├── public
|  └── index.html
├── src
|  ├── App.js
|  └── index.js
└── yarn.lock

其中,index.js 的内容如下,

import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';

ReactDOM.render(
  ,
  document.getElementById('root')
);

App.js 的内容如下,

const App = () => {
  debugger;
  return 'hello world'
};

export default App;

当前 React 项目只加载了一个 组件,这个组件只返回了一段纯文本 'hello world'

与 React 初窥门径(一):环境准备 介绍的一致,我们可以启动项目,

$ yarn
$ yarn start

# http://127.0.0.1:3000

3. 调试 Web 项目

参考 React 初窥门径(三):用 VSCode 调试,
我们将 github: thzt/react-tour 中的文件,拷贝到 React 源码根目录,

  • package.json:直接覆盖,其中新增了 debug-build 这个 npm scripts
  • .vscode/:拷贝到 React 源码目录,其中包含了两个 debug 配置,我们要用 Debug React 这个配置
  • .tour/:VSCode CodeTour 插件的数据

整体操作流程如下:
(1)示例项目操作过程

  • 下载 github: thzt/react-tour/example-project 示例项目
  • 给示例项目安装依赖:yarn
  • 替换 example-projectnode_modules 中的依赖 reactreact-dom
    参考 React 初窥门径(一):环境准备
    可使用工具 github: thzt/react-tour/tool/link.bash
  • 启动示例项目:yarn starthttp://127.0.0.1:3000

(2)React 源码操作过程

  • 下载(克隆) React 源码,并切换到 v17.0.2
  • 拷贝 github: thzt/react-tour 中的 package.json .vscode/ .tour/ 到 React 源码目录
  • 选择 Debug React 选项进行调试

我们发现 VSCode 的断点停在了 example-project/src/App.js 文件中。

const App = () => {
  debugger;              // <- 断点到了这里
  return 'hello world'
};

export default App;

4. 调用栈

我们先来跟踪一下,从 ReactDOM.render 到 App 组件 debugger 位置的调用栈,

在 CodeTour(.tour/)中,也记录了这个过程,

4.1 组件加载过程:函数组件(call stack)

render
legacyRenderSubtreeIntoContainer
unbatchedUpdates
fn
updateContainer
scheduleUpdateOnFiber
performSyncWorkOnRoot
renderRootSync
workLoopSync
performUnitOfWork
beginWork$1
beginWork
mountIndeterminateComponent
renderWithHooks

以上调用栈只展示了函数的链式调用关系,如果用缩进表示调用链路的话,它应该是这样的,

render
  legacyRenderSubtreeIntoContainer
    unbatchedUpdates
      fn
        updateContainer
          scheduleUpdateOnFiber
            performSyncWorkOnRoot
              renderRootSync
                workLoopSync
                  performUnitOfWork
                    beginWork$1
                      beginWork
                        mountIndeterminateComponent
                          renderWithHooks

它表示 render 调用了 legacyRenderSubtreeIntoContainer
legacyRenderSubtreeIntoContainer 又调用了 unbatchedUpdates
unbatchedUpdates 又调用了 fn 等等,直到最后调用了 renderWithHooks

最后 renderWithHooks 调用了函数组件 App,来到断点那里。

5. 全流程

只看调用栈的话,React 组件的加载过程还不完整,我们知道某个函数之前别调用之前,是否还调用了其他函数,
以下我们整理了从 renderApp 调用的全流程(函数前面的数字,表示缩进层次)。

4.1.1 组件加载过程:函数组件(全流程)
(下图包含代码折叠,而且只截了一部分,完整版请查看上面的链接)

6. render 和 commit 阶段

全流程包含了特别多的细碎逻辑,我们首先想弄明白的是,

  • 组件是何时被调用的,组件返回之后发生了什么(render 阶段)
  • 组件是如何展示在页面上的(commit 阶段)

这两个阶段,就是 performSyncWorkOnRoot 做的事情了,在大图中它处于这个位置,

可以看到:

  • render 阶段(renderRootSync:根据用户创建的 React 组件,创建 Fiber Tree(先从上到下 performUnitOfWork ,再从下到上 completeWork
  • commit 阶段(commitRoot:把 Fiber Tree 实际写入到 DOM 中

一图胜千言,(函数前面的数字,表示缩进层次)

[6] performSyncWorkOnRoot
  [7] renderRootSync
    [8] markRenderStarted                                         <- render 阶段开始
    [8] workLoopSync
      [9] performUnitOfWork ---- [HostRoot {tag: 3}]              <- 从 根元素 开始向下构建 Fiber Tree
        [10] beginWork$1
          [11] beginWork
            [12] updateHostRoot                                   <- 加载 根元素 HostRoot
              [13] reconcileChildren
                [14] reconcileChildFibers                         <- 创建 child 子元素
                  [15] reconcileSingleElement
                    [16] createFiberFromElement
                      [17] createFiberFromTypeAndProps
                        [18] createFiber
      [9] performUnitOfWork ---- [IndeterminateComponent {tag: 2}] ()
        [10] beginWork$1
          [11] beginWork
            [12] mountIndeterminateComponent                      <- 加载 函数组件 App
              [13] renderWithHooks
                [14] Component
              [13] reconcileChildren
                [14] mountChildFibers=reconcileChildFibers        <- 创建 child 子元素
                  [15] reconcileSingleTextNode
                    [16] deleteRemainingChildren
                    [16] createFiberFromText
                      [17] createFiber
      [9] performUnitOfWork ---- [HostText {tag: 6}] ('hello world')
        [10] beginWork$1
          [11] beginWork
            [12] updateHostText                                   <- 加载 纯文本 'hello world'
        [10] completeUnitOfWork                                   <- 开始倒着从 子节点 向上到 根节点 进行梳理
          [11] completeWork ---- [HostText {tag: 6}] ('hello world')
            [12] createTextInstance
              [13] createTextNode
                [14] createTextNode [HTMLElement] ('hello world') <- 实际创建 HTML
          [11] completeWork ---- [IndeterminateComponent {tag: 2}] ()
          [11] completeWork ---- [HostRoot {tag: 3}]
    [8] markRenderStopped                                         <- render 阶段结束
  [7] commitRoot                                                  <- commit 阶段开始

下文我们再仔细介绍 commit 阶段。


参考

github: facebook/react v17.0.2
VSCode: CodeTour
github: thzt/react-tour
github: thzt/react-tour/example-project
React 初窥门径(一):环境准备
React 初窥门径(三):用 VSCode 调试
4.1 组件加载过程:函数组件(call stack)
4.1.1 组件加载过程:函数组件(全流程)

你可能感兴趣的:([FE] React 初窥门径(四):React 组件的加载过程(render 阶段))