万字长文+图文并茂+全面解析 React 源码 - render 篇

3,176 阅读11分钟

今天想了比较久的时间,准备开启这一系列的文章,旨在对 React 系列的源码进行深度解析,其中包含但不限于 react、react-dom、react-router... 等一系列优秀的 React 系列框架,最后再一一实现这些框架的简易版本。

本篇文章将会是对 react 和 react-dom 渲染过程源码的深度解析,我们将从官方 API 以及一些简易 Demo 来进入 react 的内部世界,探讨其中奥妙。

本文解析的 react 版本为 v16.13.0,是我 fork 的官方仓库,源码地址

结构剖析

React

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

React

我们逐一分析其中比较关键的属性:

字段 解释
props Component 组件比作函数,props 就是函数的入参
context context 就是在组件树之间共享的信息
refs 访问原生 DOM 元素的集合
updater 负责 Component 组件状态的更新
_reactInternalFiber App 实例对应的 FiberNode

一个 Component 实例的大致结构我们就解析完了,我们现在需要由内到外的继续解析 Component 内部结构以及实现。

我们现在来看看 render 方法内部, 第 7 行 的内容属于 jsx 语法,是一种 html 语法格式类似的高级模板语法。这一段我们需要借鉴一下官方的一张图来进行解释:

React

从上图可以得知,jsx 语法都会被编译成 React.createElement 函数,标签属性以及标签内容都会编译成对应的入参,由此可知我们所写的 第 7 行 代码在编译后将会变成如下代码:

React.createElement("section", {}, "Hello World");

React.createElement 所创建的对象就是 虚拟 DOM 树,那么内部创建的工作流程是什么样的?带着这个问题,我们进入下一个章节。

React.createElement

我们刚才得知 jsx 语法将会被编译成 React.createElement 函数调用,而这个函数属于 React 对象上的一个方法,现在我们就可以开始进入到源码解析,查看内部实现。

React
React

上图就是 React.createElement,我们先看最后返回的结果是 ReactElement 函数的执行结果,该函数最后返回的是一个 React Element 对象(后面会提到)。

所以 React.createElement 其实是一个工厂函数,用于创建 React Element 对象,我们再来看看这个工厂函数主要做了哪些工作。

  • 11-29 行:收集了 config 中的一些字段,并且将其他非内置字段添加到 props 对象中;
  • 31-40 行:将入参中的 children 参数挂载到 propschildren 字段中;(本示例中 "Hello World" 就是一个 “children”)
  • 42-49 行:收集组件(type 可能是字符串也有可能是 Component 实例,例如 <section /><App />)中设置的 defaultProps 属性;

在完成一系列的初始化工作后,进入了 ReactElement 的创建工作(见下图)。

React

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

我们最后对 React Element 的创建过程画一个流程图来加深理解。(见下图)

React

React.Component

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

React

Component 属于一个构造函数(见上图),Component 定义了几个属性,分别是 props、context、refs、updater,这些属性在之前已经解释过,这里不再复述。这里需要注意的是 Component 中的两个方法 setStateforceUpdate,调用的都是内部 updater 的方法进行事件通知,将数据和 UI 更新的任务交给了内部的 updater 去处理,符合 单一职责设计原则

到这里,Component 类的结构已经解析完成了。什么,这就解析完成了?生命周期函数呢?渲染过程呢?一个都还没有看到啊。别着急,由于 React 内部的职责划分与不同平台实现,所以这部分根据不同平台的实现被放在了 react-domreact-native 中。我们接下来就对我们常用的浏览器端,react-dom 中渲染过程以及对组件生命周期的处理进行详细的梳理。在此之前,放张图对本章的 Component 进行小结。

React.Component

渲染过程(react-dom

render 函数

在解析完了 React.ElementReact.Component 之后,可能很多人只是了解到了基础结构体的创建,还是感觉云里雾里。现在我们来理一理 react-dom 的整个渲染过程以及组件生命周期,从 constructor 组件的创建到 componentDidMount() 组件的挂载,最后再画一个流程图来进行总结。

react 本身只是一些基础类的创建,比如 React.ElementReact.Component,而后续的流程则根据不同的平台有不同的实现。我们这里以我们常用的浏览器环境为例,调用的是 ReactDOM.render() 方法(见下图),我们现在就来对这个方法的渲染过程做一个详细解析。

ReactDOM.render()
ReactDOM.render()

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

React

FiberNode

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

React

legacyRenderSubtreeIntoContainer 中的 第 28 行,就是 FiberNode 树 的创建过程。

FiberNode 由内部的 createFiber 函数进行创建(见下图)。(这也是 React16 版本后作出的巨大更新,这个后面我们再展开说)。

React

FiberNode 被创建后挂载在了 FiberRoot.current 上。最后,App 组件作为根组件实例被返回,而接下来的渲染过程由 FiberNode 接管。

我们画一个流程图来帮助理解(见下图)。

React

从上图可以看出,我们的 React Element 作为 render 函数的入参,创建了一个 FiberNode 实例,也就是 FiberRoot.current,而后续的渲染过程都由这个根 FiberNode 接管,包括所有的生命周期。

递归构建 FiberNode 树

在构建完了根 FiberNode 实例后,第 40 行 调用了 updaterContainer 函数开始构建整棵 FiberNode 树以及完成 DOM 渲染(见下图)。

React

React

updaterContainer 是一个比较关键的函数,我们来解析一下这个函数做了什么:

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

FiberNode 实例

Element 实例

  • 第 32 行:内部开始递归调度,创建 FiberNode 树。创建一个工作节点快照 workInProgress(初始值是根 FiberNode),围绕着 workInProgressupdateQueue 展开构建工作(见下图);

根据 updateQueue 更新节点(performUnitOfWork 将返回 workInProgress.child,直到所有节点遍历完成)

更新过程

创建 FiberNode 子节点

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

beginWork

beginWork 函数会根据 propscontext 是否改变(第 12~15 行)、当前当前节点优先级是否高于正在更新的节点优先级(第 17 行)这两项来决定当前节点是否需要更新。

然后根据节点的标签类型(tag),调用不同的函数进行内部状态更新。(见下图)

beginWork

Root(FiberNode) 节点更新 - updateHostRoot

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

updateHostRoot

updateHostRoot

按照惯例,我们逐行解析函数所做的事情:

  • 第 2 行:将一系列有用的信息推入内部栈(其中包括 #app 实例、context 信息等等)。
  • 第 5~7 行:收集节点新的 props 属性和旧的 state、children 属性。
  • 第 8 行:浅复制更新队列,防止引用属性互相影响;
  • 第 9 行:执行更新队列,主要的任务是将 React.Element 添加到 FibermemoizedStateupdateQueue 更新队列中(见下图);

React

  • 第 36~45 行:对上一步的 memoizedState 中的 element 进行进一步的处理,将其封装成 FiberNode 然后挂载在 workInProgress(当前工作节点快照).child  属性上,最后将该 child 返回。

到这一步,FiberNode 树的第一个节点就已经构建完成并挂载,我们来画一张流程图进行梳理(下图)。

React

App Component(FiberNode) 更新流程 - updateClassComponent

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

React

constructClassInstance

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

updateClassComponent

constructClassInstance

constructClassInstance 函数中(见上图 1):

  • 第 96 行 创建 App Component 实例。
  • 第 101 行 将实例挂载在 workInProgressstateNode 属性中(件上图 2)
  • 第 107 行 最后返回该实例。

mountClassInstance

constructClassInstance 执行完成后,接下来执行第二个关键函数 mountClassInstance

constructClassInstance

React

mountClassInstance 函数中对 Component 实例进行挂载的一些初始化工作(见上图)。我们从上图可以看出,到了这里就开始了 Component 的生命周期钩子逻辑。

在初始化实例的一些基础属性后,第 136~145 行执行了 Component 的第一个生命周期钩子,也就是 getDerivedStateFromProps(见上图),它使用返回的对象来更新 state

而紧随其后(见下图) 第 153 行 触发了第二个生命周期钩子 componentWillMount,主要用于在挂载前执行一些操作。

React

React

finishClassComponent

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

React

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

React

React

React Element(FiberNode) 更新流程 - updateHostComponent

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

React

React

第 13 行 会对组件的 children 类型进行判断,判断是否为纯文本内容,我们在此处就是纯文本(section 标签内的 Hello World 文本),随后 nextChildren 就将被置空。

到这里,nextChildren 已经为空,完整的 FiberNode 树就已经构建完成。beginWork 结束,接下来进入到新的流程。

创建 真实 DOM 树

在结束了 beginWork 流程后,将调用 createInstance 函数创建 真实 DOM 树(见下图)。

React

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

React

React

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

React

真实 DOM 树构建完成后,并且此时 workInProgress.child 也为 null,本次 workLoopSync 流程将在此结束,接下来进入到 finishSyncRender 函数,进行节点的渲染工作。

渲染真实 DOM

react-dom 将在回调函数内部将调用 insertOrAppendPlacementNodeIntoContainer 方法对 FiberNode 进行遍历。(见下图)

React

由上图可知该函数会对 Host 节点(带有 html tag 结构的节点)调用 appendChildToContainer 函数进行渲染,其他节点取其 child 值进行递归调用。

appendChildToContainer 函数内部,通过 appendChildFiberNode 上的 stateNode (我们在上一步创建好的 真实 DOM 树)添加到 container(#app)中,然后调用 componentDidMount 生命周期钩子函数。(见下图)

React

React

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

React

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

React

在本篇文章主要是解析 React 的整个渲染过程,对 React 的整体结构和工作流程有了个初步的了解,下一章将围绕着 Component 的生命周期与 setState 进行解析,如有需要可以关注专栏后续更新。

还有一件事

如果本文对你有帮助的话,请你动动小鼠标点个赞吧~

因为这么做对作者来说是很大的一个激励~

据说点赞的人都找到对象了~

原文地址