0%

React探索之组件构建及处理流程

开发环境

使用 create-react-app 创建 demo 项目。

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
╰─○ npx create-react-app react-demo

npx: 67 安装成功,用时 10.485 秒

Creating a new React app in /Users/chujiayi/StudyStation/React/react-demo.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...

yarn add v1.22.10
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.20.0" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 7 new dependencies.
info Direct dependencies
├─ cra-template@1.1.2
├─ react-dom@17.0.2
├─ react-scripts@4.0.3
└─ react@17.0.2
info All dependencies
├─ cra-template@1.1.2
├─ immer@8.0.1
├─ react-dev-utils@11.0.4
├─ react-dom@17.0.2
├─ react-scripts@4.0.3
├─ react@17.0.2
└─ scheduler@0.20.2
✨  Done in 162.25s.

项目结构如下图所示:

js代码

执行start 命令:

1
2
yarn start
// 源码:node_modules/react-scripts/scripts/start.js

ReactDOM.render 方法

方法声明

查看项目入口文件 index.js*,我们发现里面有一个 *render 方法,这也是整个 React 组件渲染的入口方法。我们就从这个方法开始,一步一步去看 React 的整个工作流程。

1
2
3
4
5
6
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

查看 ReactDOM **源码,render 方法的声明为:

1
function render(element, container, callback) { ... }

render 方法第一个参数为 element*,对应要渲染的 *ReactElement 对象;第二个参数为 container,对应要挂载的DOM节点;第三个参数为 callback,对应渲染完成后的回调。

其中后两个参数比较直观,我们传入的是 root 节点和空的回调函数。但是第一个 element 参数,我们传入的明明是一段 JSX 代码,怎么就变成一个 ReactElement 对象了呢。

ReactElement 创建

了解 React 的同学都知道,浏览器并不认识 JSX 代码,所以在运行之前需要被编译为普通的 JS 代码,这一步会在打包过程中完成。

index.js 中的这段代码,会被编译为如下 JS 代码:

js代码

1
2
3
4
5
6
7
8
9
10
11
react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render( /*#__PURE__*/Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__["jsxDEV"])(react__WEBPACK_IMPORTED_MODULE_0___default.a.StrictMode, {
children: /*#__PURE__*/Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_5__["jsxDEV"])(_App__WEBPACK_IMPORTED_MODULE_3__["default"], {}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 9,
columnNumber: 5
}, undefined)
}, void 0, false, {
fileName: _jsxFileName,
lineNumber: 8,
columnNumber: 3
}, undefined), document.getElementById('root'));

看着可能有些复杂,我们可以精简为如下代码。

1
render(jsxDEV(Symbol(react.strict_mode, {children: jsxDEV(App, {}, undefined, source, undefined)}, undefined, source, undefined), document.getElementById('root'));

还是看不清楚?我们把嵌套逻辑拆出来一行一行看下。

1
2
3
4
5
6
// 创建 appElement,对应 <App />
const appElement = jsxDEV(App, {}, undefined, source, undefined);
// 对应 <React.StrictMode>,包含一个child元素(appElement)
const strictModeElement = jsxDEV(Symbol(react.strict_mode), {children: appElement}, undefined, source, undefined);
// 渲染 strictModeElement 并挂载到 root 节点
render(strictModeElement, document.getElementById('root'));

其中有一个 jsxDEV 方法,其作用为将 JSX 代码转换为 DOMElement 对象。

1
function jsxDEV(type, config, maybeKey, source, self);

React 17 提供了新的 jsx 方法,用于进行 JSX 转换,开发模式下该方法名为 jsxDEV 。 旧版本的 JSX 转换方法为 React.createElement,这个方法大家应该很熟悉了。
为什么 React 会提供一个新的 JSX 转换方法,可参考 https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html。

经过上面的分析,我们知道 index.js 中的 render 方法最终会被转换为如下形式:

1
2
3
// 渲染 strictModeElement 并挂载到 root 节点
// strictModeElement 包含一个子节点 appElement
render(strictModeElement, document.getElementById('root'));

FiberNode 创建

workRootSync 循环

render 方法的第一步,就是创建一个 root 节点(姑且称之为 rootFiberNode)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function renderRootSync(root, lanes) {
// ...
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// 创建 root 节点
prepareFreshStack(root, lanes);
startWorkOnPendingInteractions(root, lanes);
}
do {
try {
// 循环创建子节点
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
// ...
}

renderRootSync 方法中,除了创建 rootFiberNode 外,还有非常重要的一个方法 workLoopSync,该方法的作用是循环创建 childern **FiberNode。

workLoopSync 方法的源码非常简单,就是循环处理 workInProgress 对象。workInProgress是个全局对象,React 就是通过不断更新该对象,实现对所有子节点的处理。

1
2
3
4
5
6
7
function workLoopSync() {
// Already timed out, so perform work without checking if we need to yield.
while (workInProgress !== null) {
console.log('perform unit: ', workInProgress);
performUnitOfWork(workInProgress);
}
}

performUnitOfWork

performUnitOfWork 里具体做了什么事情呢?我们一步步往下看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function performUnitOfWork(unitOfWork) {
// ...
// 记录下一个要处理的节点
var next;
if ((unitOfWork.mode & ProfileMode) !== NoMode) {
startProfilerTimer(unitOfWork);
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
}
// ...
if (next === null) {
// 没有next,表示处理完成
// 注意并不是完成了整个处理流程,只是完成了当前的子流程,具体逻辑后面分析 completeUnitOfWork 时会详细介绍
completeUnitOfWork(unitOfWork);
} else {
// 将下一个要处理的节点赋值给 workInProgress,使得 workLoopSync 可以继续循环执行
workInProgress = next;
}
// ...
}

beginWork$1*方法内部会调用 *beginWork 方法,beginWork 方法代码很长,我们只截取重点部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case IndeterminateComponent:
// <App /> 对应的 fiberNode 会走到这里
return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
case HostRoot:
// root 节点(FiberNode) 会走到这里,并返回child,也就是 <React.StrictMode /> 对应的 fiberNode
return updateHostRoot(current, workInProgress, renderLanes);
case Mode:
// <React.StrictMode /> 对应的 fiberNode 会走到这里,并返回child,也就是 <App /> 对应的 fiberNode
return updateMode(current, workInProgress, renderLanes);
}
}

该方法主要工作流程为:

  1. rootFiberNode 执行 updateHostRoot 方法,处理 rootFiberNode,并生成 <React.StrictMode /> 对应的 fiberNode(姑且称之为 *strictModeFiberNode)。此时全局对象 workInProgress被赋值为 strictModeFiberNode,继续 *workRootSync 循环。
  2. strictModeFiberNode 执行 updateMode 方法,处理 strictModeFiberNode ,并生成 对应的 fiberNode(姑且称之为 appFiberNode)。此时全局对象 *workInProgress被赋值为 appFiberNode,继续 *workRootSync 循环。
  3. appFiberNode 执行 mountInterminateComponent 方法,处理 appFiberNode,并生成… 等下!appFiberNode 的子节点是什么?好像还没创建过?

appFiberNode 的子节点确实还没有创建,但是先不着急,我们先看下目前的 Element Tree:

Element Tree

同时,每个 Element 都会有对应的 FiberNode,相应的也就形成了一个 FiberNode Tree。

为了能更直观地体现其工作流程,我们可以分别在 workLoopSync*和 *jsxDEV 方法内部添加调试日志,然后刷新页面。

日志

最上面两条 jsxDev方法的日志,对应于前面提到的 JSX 转换逻辑。

  1. 创建 对应的 ReactElement。
  2. 创建 <React.StrictMode> 对应的 ReactElement。

接下来的日志对应 FiberNode 的创建和处理流程。

  1. 处理 rootFiberNode
  2. 处理 strictModeFiberNode
  3. 处理 appFiberNode

App Element Tree 构建

接下来我们继续看上面遗留的问题,App 组件内部的 Element Tree 是如何被创建的。

创建 App 组件

在上面的 switch…case… 逻辑中,对 appFiberNode 会执行 mountInterminateComponent 方法,mountInterminateComponent 会调用 renderWithHooks 方法完成 组件的创建。

1
2
3
4
5
6
7
function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) {
// ...
// 创建 Component
var children = Component(props, secondArg);
// ...
return children;
}

其中的 Component 参数就是用于创建组件的方法,此例中即为 App 方法,也就是项目中 App.js 里的这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}

App 方法内部返回的又是一段 JSX 代码,我们很容易就联想到前问提到的 jsxjsxDEV 方法。App 方法返回值即为该组件对应的 ReactElement 对象。

Element Tree 构建

App 方法执行后,当前的 Element Tree 如下图所示。

Element Tree

继续看 workLoopSync*和 *jsxDEV 方法内部的调试日志,输出结果就很清晰了。

日志

整体执行流程如下:

  1. 创建 App 组件对应的 Element
  2. 创建 React.StrictMode 组件对应的 Element
  3. 处理 root FiberNode
  4. 处理 React.StrictMode 对应的 FiberNode
  5. 处理 App 对应的 FiberNode
  6. 依次创建 App 内部的组件对应的 Element
  7. 依次处理 App 内部组件对应的 FiberNode

Element Tree 处理顺序

我们详细看上面的日志的话,会发现 App 内部的处理流程为

1
div -> header -> img -> p -> text -> code -> text -> a

对应着 Element Tree:

Element Tree

可以看出,对 Element Tree 进行处理时,首先采用深度优先算法,找到最底层的元素;

  1. 深度优先,直到最底层的节点。
  2. 最底层的节点没有子节点,继续找最底层节点的兄弟(sibling)节点。
  3. 找到兄弟节点,继续第1步;否则结束。

这也就能解释我们在 performUnitOfWork 一节中提到 performUnitOfWork 方法时,遗留了一个问题:completeUnitOfWork 只是完成了当前的子流程,而不是整体流程。

1
2
3
4
5
6
7
8
9
10
11
12
function performUnitOfWork(unitOfWork) {
// ...
if (next === null) {
// 没有next,表示处理完成
// 注意并不是完成了整个处理流程,只是完成了当前的子流程,具体逻辑后面分析 completeUnitOfWork 时会详细介绍
completeUnitOfWork(unitOfWork);
} else {
// 将下一个要处理的节点赋值给 workInProgress,使得 workLoopSync 可以继续循环执行
workInProgress = next;
}
// ...
}

执行到 completeUnitOfWork 时,表示当前最底层的节点已经找到,没有子节点了。completeUnitOfWork 方法内部会将全部变量 workInProgress 赋值为其兄弟节点,也就是上面流程中的第2步。

因此,completeUnitOfWork 只是结束了当前子树的深度遍历流程,并没有完成整体 Element Tree 的处理流程。

总结

本文重点介绍了 React 中组件的构建及处理流程,以 ReactDOM.render 方法为切入点,分别介绍了 ReactElement 的创建、FiberNode 的创建以及 Element Tree 的处理流程等。很多细节问题并没有做过多介绍,后面的文章会有进一步的说明。