今天想了比较久的时间,准备开启这一系列的文章,旨在对 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
实例,例如和
)中设置的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
)(见下图),我们的渲染流程解析宣告完成!
最后也是按照惯例,用一张流程图来梳理我们的整个渲染过程。
在本篇文章主要是解析 React
的整个渲染过程,对 React
的整体结构和工作流程有了个初步的了解,下一章将围绕着 Component
的生命周期与 setState
进行解析,如有需要可以关注专栏后续更新。