开发环境
使用 create-react-app 创建 demo 项目。
1 | ╰─○ npx create-react-app react-demo |
项目结构如下图所示:
执行start 命令:
1 | yarn start |
ReactDOM.render 方法
方法声明
查看项目入口文件 index.js*,我们发现里面有一个 *render 方法,这也是整个 React 组件渲染的入口方法。我们就从这个方法开始,一步一步去看 React 的整个工作流程。
1 | ReactDOM.render( |
查看 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 代码:
1 | 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, { |
看着可能有些复杂,我们可以精简为如下代码。
1 | render(jsxDEV(Symbol(react.strict_mode, {children: jsxDEV(App, {}, undefined, source, undefined)}, undefined, source, undefined), document.getElementById('root')); |
还是看不清楚?我们把嵌套逻辑拆出来一行一行看下。
1 | // 创建 appElement,对应 <App /> |
其中有一个 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 | // 渲染 strictModeElement 并挂载到 root 节点 |
FiberNode 创建
workRootSync 循环
render 方法的第一步,就是创建一个 root 节点(姑且称之为 rootFiberNode)。
1 | function renderRootSync(root, lanes) { |
在 renderRootSync 方法中,除了创建 rootFiberNode 外,还有非常重要的一个方法 workLoopSync,该方法的作用是循环创建 childern **FiberNode。
workLoopSync 方法的源码非常简单,就是循环处理 workInProgress 对象。workInProgress是个全局对象,React 就是通过不断更新该对象,实现对所有子节点的处理。
1 | function workLoopSync() { |
performUnitOfWork
performUnitOfWork 里具体做了什么事情呢?我们一步步往下看。
1 | function performUnitOfWork(unitOfWork) { |
beginWork$1*方法内部会调用 *beginWork 方法,beginWork 方法代码很长,我们只截取重点部分。
1 | function beginWork(current, workInProgress, renderLanes) { |
该方法主要工作流程为:
- 对 rootFiberNode 执行 updateHostRoot 方法,处理 rootFiberNode,并生成 <React.StrictMode /> 对应的 fiberNode(姑且称之为 *strictModeFiberNode)。此时全局对象 workInProgress被赋值为 strictModeFiberNode,继续 *workRootSync 循环。
- 对 strictModeFiberNode 执行 updateMode 方法,处理 strictModeFiberNode ,并生成
对应的 fiberNode(姑且称之为 appFiberNode)。此时全局对象 *workInProgress被赋值为 appFiberNode,继续 *workRootSync 循环。 - 对 appFiberNode 执行 mountInterminateComponent 方法,处理 appFiberNode,并生成… 等下!appFiberNode 的子节点是什么?好像还没创建过?
appFiberNode 的子节点确实还没有创建,但是先不着急,我们先看下目前的 Element Tree:
同时,每个 Element 都会有对应的 FiberNode,相应的也就形成了一个 FiberNode Tree。
为了能更直观地体现其工作流程,我们可以分别在 workLoopSync*和 *jsxDEV 方法内部添加调试日志,然后刷新页面。
最上面两条 jsxDev方法的日志,对应于前面提到的 JSX 转换逻辑。
- 创建
对应的 ReactElement。 - 创建 <React.StrictMode> 对应的 ReactElement。
接下来的日志对应 FiberNode 的创建和处理流程。
- 处理 rootFiberNode
- 处理 strictModeFiberNode
- 处理 appFiberNode
App Element Tree 构建
接下来我们继续看上面遗留的问题,App 组件内部的 Element Tree 是如何被创建的。
创建 App 组件
在上面的 switch…case… 逻辑中,对 appFiberNode 会执行 mountInterminateComponent 方法,mountInterminateComponent 会调用 renderWithHooks 方法完成
1 | function renderWithHooks(current, workInProgress, Component, props, secondArg, nextRenderLanes) { |
其中的 Component 参数就是用于创建组件的方法,此例中即为 App 方法,也就是项目中 App.js 里的这个方法。
1 | function App() { |
App 方法内部返回的又是一段 JSX 代码,我们很容易就联想到前问提到的 jsx 和 jsxDEV 方法。App 方法返回值即为该组件对应的 ReactElement 对象。
Element Tree 构建
App 方法执行后,当前的 Element Tree 如下图所示。
继续看 workLoopSync*和 *jsxDEV 方法内部的调试日志,输出结果就很清晰了。
整体执行流程如下:
- 创建 App 组件对应的 Element
- 创建 React.StrictMode 组件对应的 Element
- 处理 root FiberNode
- 处理 React.StrictMode 对应的 FiberNode
- 处理 App 对应的 FiberNode
- 依次创建 App 内部的组件对应的 Element
- 依次处理 App 内部组件对应的 FiberNode
Element Tree 处理顺序
我们详细看上面的日志的话,会发现 App 内部的处理流程为
1 | div -> header -> img -> p -> text -> code -> text -> a |
对应着 Element Tree:
可以看出,对 Element Tree 进行处理时,首先采用深度优先算法,找到最底层的元素;
- 深度优先,直到最底层的节点。
- 最底层的节点没有子节点,继续找最底层节点的兄弟(sibling)节点。
- 找到兄弟节点,继续第1步;否则结束。
这也就能解释我们在 performUnitOfWork 一节中提到 performUnitOfWork 方法时,遗留了一个问题:completeUnitOfWork 只是完成了当前的子流程,而不是整体流程。
1 | function performUnitOfWork(unitOfWork) { |
执行到 completeUnitOfWork 时,表示当前最底层的节点已经找到,没有子节点了。completeUnitOfWork 方法内部会将全部变量 workInProgress 赋值为其兄弟节点,也就是上面流程中的第2步。
因此,completeUnitOfWork 只是结束了当前子树的深度遍历流程,并没有完成整体 Element Tree 的处理流程。
总结
本文重点介绍了 React 中组件的构建及处理流程,以 ReactDOM.render 方法为切入点,分别介绍了 ReactElement 的创建、FiberNode 的创建以及 Element Tree 的处理流程等。很多细节问题并没有做过多介绍,后面的文章会有进一步的说明。