今天想了比较久的时间,准备开启这一系列的文章,旨在对 React 系列的源码进行深度解析,其中包含但不限于 react、react-dom、react-router... 等一系列优秀的 React 系列框架,最后再一一实现这些框架的简易版本。
本篇文章将会是对 react 和 react-dom 渲染过程源码的深度解析,我们将从官方 API 以及一些简易 Demo 来进入 react 的内部世界,探讨其中奥妙。
本文解析的
react版本为v16.13.0,是我 fork 的官方仓库,源码地址。
结构剖析

我们先从最基础的结构开始解析,从上面这张图来看看。我们创建了一个 App 类,继承于 React.Component 类,在 render 生命周期函数中返回了一个 jsx 格式的 html 标签集合。我们打开控制台,查看创建的实例(如下图):

我们逐一分析其中比较关键的属性:
| 字段 | 解释 |
|---|---|
props |
把 Component 组件比作函数,props 就是函数的入参 |
context |
context 就是在组件树之间共享的信息 |
refs |
访问原生 DOM 元素的集合 |
updater |
负责 Component 组件状态的更新 |
_reactInternalFiber |
App 实例对应的 FiberNode
|
一个 Component 实例的大致结构我们就解析完了,我们现在需要由内到外的继续解析 Component 内部结构以及实现。
我们现在来看看 render 方法内部, 第 7 行 的内容属于 jsx 语法,是一种 html 语法格式类似的高级模板语法。这一段我们需要借鉴一下官方的一张图来进行解释:

从上图可以得知,jsx 语法都会被编译成 React.createElement 函数,标签属性以及标签内容都会编译成对应的入参,由此可知我们所写的 第 7 行 代码在编译后将会变成如下代码:
React.createElement("section", {}, "Hello World");
而 React.createElement 所创建的对象就是 虚拟 DOM 树,那么内部创建的工作流程是什么样的?带着这个问题,我们进入下一个章节。
React.createElement
我们刚才得知 jsx 语法将会被编译成 React.createElement 函数调用,而这个函数属于 React 对象上的一个方法,现在我们就可以开始进入到源码解析,查看内部实现。


上图就是 React.createElement,我们先看最后返回的结果是 ReactElement 函数的执行结果,该函数最后返回的是一个 React Element 对象(后面会提到)。
所以 React.createElement 其实是一个工厂函数,用于创建 React Element 对象,我们再来看看这个工厂函数主要做了哪些工作。
-
11-29 行:收集了config中的一些字段,并且将其他非内置字段添加到props对象中; -
31-40 行:将入参中的children参数挂载到props的children字段中;(本示例中"Hello World"就是一个 “children”) -
42-49 行:收集组件(type可能是字符串也有可能是Component实例,例如<section />和<App />)中设置的defaultProps属性;
在完成一系列的初始化工作后,进入了 ReactElement 的创建工作(见下图)。

ReactElement 函数就比较一目了然了,返回了一个 element(React Element) 对象。React Element 对象其实就是一棵虚拟 DOM 树($$typeof 字段表示了这是一个 React Element 类型),包含了标签和属性(attribute)信息。Component 执行 render 函数得到 虚拟 DOM 树,再通过 react-dom 将其包装成 FiberNode,然后被解析成 真实 DOM 树 后渲染在页面中(对应的容器内),这个我们后续再详细解析,这里就不展开了。
我们最后对 React Element 的创建过程画一个流程图来加深理解。(见下图)

React.Component
我们接下来要对 React.Component 进行进一步的解析,看看 Component 整体的运行逻辑以及是如何使用 React.Element 的。

Component 属于一个构造函数(见上图),Component 定义了几个属性,分别是 props、context、refs、updater,这些属性在之前已经解释过,这里不再复述。这里需要注意的是 Component 中的两个方法 setState 和 forceUpdate,调用的都是内部 updater 的方法进行事件通知,将数据和 UI 更新的任务交给了内部的 updater 去处理,符合 单一职责设计原则。
到这里,Component 类的结构已经解析完成了。什么,这就解析完成了?生命周期函数呢?渲染过程呢?一个都还没有看到啊。别着急,由于 React 内部的职责划分与不同平台实现,所以这部分根据不同平台的实现被放在了 react-dom 或 react-native 中。我们接下来就对我们常用的浏览器端,react-dom 中渲染过程以及对组件生命周期的处理进行详细的梳理。在此之前,放张图对本章的 Component 进行小结。

渲染过程(react-dom)
render 函数
在解析完了 React.Element 和 React.Component 之后,可能很多人只是了解到了基础结构体的创建,还是感觉云里雾里。现在我们来理一理 react-dom 的整个渲染过程以及组件生命周期,从 constructor 组件的创建到 componentDidMount() 组件的挂载,最后再画一个流程图来进行总结。
react 本身只是一些基础类的创建,比如 React.Element 和 React.Component,而后续的流程则根据不同的平台有不同的实现。我们这里以我们常用的浏览器环境为例,调用的是 ReactDOM.render() 方法(见下图),我们现在就来对这个方法的渲染过程做一个详细解析。


从上图可以看出,render 函数返回 legacyRenderSubtreeIntoContainer 函数的调用,而该函数最终返回的结果是 Component 实例(也就是 App 组件,见下图)。

FiberNode
我们来看看 render 函数内部调用的 legacyRenderSubtreeIntoContainer 函数(见下图)

在 legacyRenderSubtreeIntoContainer 中的 第 28 行,就是 FiberNode 树 的创建过程。
FiberNode 由内部的 createFiber 函数进行创建(见下图)。(这也是 React 在 16 版本后作出的巨大更新,这个后面我们再展开说)。

FiberNode 被创建后挂载在了 FiberRoot.current 上。最后,App 组件作为根组件实例被返回,而接下来的渲染过程由 FiberNode 接管。
我们画一个流程图来帮助理解(见下图)。

从上图可以看出,我们的 React Element 作为 render 函数的入参,创建了一个 FiberNode 实例,也就是 FiberRoot.current,而后续的渲染过程都由这个根 FiberNode 接管,包括所有的生命周期。
递归构建 FiberNode 树
在构建完了根 FiberNode 实例后,第 40 行 调用了 updaterContainer 函数开始构建整棵 FiberNode 树以及完成 DOM 渲染(见下图)。


updaterContainer 是一个比较关键的函数,我们来解析一下这个函数做了什么:
-
第 8~14 行:React内部的更新任务设置了优先级大小,优先级较高的更新任务将会中断优先级较低的更新任务。React设置了ExpirationTime任务过期时间,如果时间到期后任务仍未执行(一直被打断),则会强制执行该更新任务。同时,React内部也会将过期时间相近的更新任务合并成一个(批量)更新任务,从而达到批量更新减少消耗的效果。(React setState “异步” 更新原理) -
第 16~21 行:从父组件中收集context属性(由于这里是root组件,所以父组件为空)。 -
第 23~31 行:构建更新队列,第 24 行将Element实例(见下图 1)挂载在update对象上,第 31 行将更新队列(updateQueue) 挂载在FiberNode实例(见下图 2)。


-
第 32 行:内部开始递归调度,创建FiberNode树。创建一个工作节点快照workInProgress(初始值是根FiberNode),围绕着workInProgress对updateQueue展开构建工作(见下图);
根据
updateQueue更新节点(performUnitOfWork将返回workInProgress.child,直到所有节点遍历完成)

创建 FiberNode 子节点
进入 performUnitOfWork 函数内部,我们省略掉一系列目前不需要关注的函数,首先进入到 beginWork 函数(见下图)。

beginWork 函数会根据 props 和 context 是否改变(第 12~15 行)、当前当前节点优先级是否高于正在更新的节点优先级(第 17 行)这两项来决定当前节点是否需要更新。
然后根据节点的标签类型(tag),调用不同的函数进行内部状态更新。(见下图)

Root(FiberNode) 节点更新 - updateHostRoot
我们第一次进入是 root 节点,所以进入到 updateHostRoot 函数内部逻辑进行处理。(见下图)


按照惯例,我们逐行解析函数所做的事情:
-
第 2 行:将一系列有用的信息推入内部栈(其中包括#app实例、context信息等等)。 -
第 5~7 行:收集节点新的props属性和旧的state、children属性。 -
第 8 行:浅复制更新队列,防止引用属性互相影响; -
第 9 行:执行更新队列,主要的任务是将React.Element添加到Fiber的memoizedState和updateQueue更新队列中(见下图);

-
第 36~45 行:对上一步的memoizedState中的element进行进一步的处理,将其封装成FiberNode然后挂载在workInProgress(当前工作节点快照).child属性上,最后将该child返回。
到这一步,FiberNode 树的第一个节点就已经构建完成并挂载,我们来画一张流程图进行梳理(下图)。

App Component(FiberNode) 更新流程 - updateClassComponent
接下来就是对子节点的依次更新流程(见下图),也就是 App Component 对应的 FiberNode。依然是 beginWork 函数,在 第 232~246 行 调用我们的 App Component 节点的更新流程。

constructClassInstance
在 updateClassComponent 函数中,有三个关键函数,第一个就是 constructClassInstance。


在 constructClassInstance 函数中(见上图 1):
-
第 96 行创建App Component实例。 -
第 101 行将实例挂载在workInProgress的stateNode属性中(件上图 2) -
第 107 行最后返回该实例。
mountClassInstance
在 constructClassInstance 执行完成后,接下来执行第二个关键函数 mountClassInstance。


mountClassInstance 函数中对 Component 实例进行挂载的一些初始化工作(见上图)。我们从上图可以看出,到了这里就开始了 Component 的生命周期钩子逻辑。
在初始化实例的一些基础属性后,第 136~145 行执行了 Component 的第一个生命周期钩子,也就是 getDerivedStateFromProps(见上图),它使用返回的对象来更新 state。
而紧随其后(见下图) 第 153 行 触发了第二个生命周期钩子 componentWillMount,主要用于在挂载前执行一些操作。


finishClassComponent
在实例创建完成并且调用了上面两个生命周期钩子后,进入到最后一个关键函数 finishClassComponent。

在 finishClassComponent 中 render 函数(见上图)。而 render 函数执行返回的就是 React.Element(虚拟 DOM 树)(下图 1),最后将其包装成 FiberNode 后返回(下图 2)后进入进入 workLoopSync 流程。


React Element(FiberNode) 更新流程 - updateHostComponent
还是 beginWork 函数(见下图),进入 updateHostComponent 进行 React Element(FiberNode) 组件更新阶段。


在 第 13 行 会对组件的 children 类型进行判断,判断是否为纯文本内容,我们在此处就是纯文本(section 标签内的 Hello World 文本),随后 nextChildren 就将被置空。
到这里,nextChildren 已经为空,完整的 FiberNode 树就已经构建完成。beginWork 结束,接下来进入到新的流程。
创建 真实 DOM 树
在结束了 beginWork 流程后,将调用 createInstance 函数创建 真实 DOM 树(见下图)。

在 createInstance 内部调用了 createElement 函数创建了 真实 DOM 节点(见下图 1),然后通过递归遍历 props 中的属性(包括 children)构建了一棵 真实 DOM 树(见下图 2)


通过调用 createInstance 方法创建真实 DOM(此时还没有插入到文档中)后,然后将 DOM 树 对象挂载到 FiberNode 的 stateNode 属性上(见下图)。

在 真实 DOM 树构建完成后,并且此时 workInProgress.child 也为 null,本次 workLoopSync 流程将在此结束,接下来进入到 finishSyncRender 函数,进行节点的渲染工作。
渲染真实 DOM
react-dom 将在回调函数内部将调用 insertOrAppendPlacementNodeIntoContainer 方法对 FiberNode 进行遍历。(见下图)

由上图可知该函数会对 Host 节点(带有 html tag 结构的节点)调用 appendChildToContainer 函数进行渲染,其他节点取其 child 值进行递归调用。
在 appendChildToContainer 函数内部,通过 appendChild 将 FiberNode 上的 stateNode (我们在上一步创建好的 真实 DOM 树)添加到 container(#app)中,然后调用 componentDidMount 生命周期钩子函数。(见下图)


到了这一步,页面中就渲染了我们在 render 中设置的 jsx 语法标签(Hello World)(见下图),我们的渲染流程解析宣告完成!

最后也是按照惯例,用一张流程图来梳理我们的整个渲染过程。