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

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

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

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

结构剖析

万字长文+图文并茂+全面解析 React 源码 - render 篇_第1张图片

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第2张图片

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

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

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第3张图片

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

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

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

React.createElement

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第4张图片
万字长文+图文并茂+全面解析 React 源码 - render 篇_第5张图片

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

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

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第6张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第7张图片

React.Component

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第8张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第9张图片

渲染过程(react-dom

render 函数

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第10张图片
万字长文+图文并茂+全面解析 React 源码 - render 篇_第11张图片

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第12张图片

FiberNode

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第13张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第14张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第15张图片

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

递归构建 FiberNode 树

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第16张图片

万字长文+图文并茂+全面解析 React 源码 - render 篇_第17张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第18张图片

万字长文+图文并茂+全面解析 React 源码 - render 篇_第19张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第20张图片

创建 FiberNode 子节点

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第21张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第22张图片

Root(FiberNode) 节点更新 - updateHostRoot

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第23张图片

万字长文+图文并茂+全面解析 React 源码 - render 篇_第24张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第25张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第26张图片

App Component(FiberNode) 更新流程 - updateClassComponent

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第27张图片

constructClassInstance

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第28张图片

万字长文+图文并茂+全面解析 React 源码 - render 篇_第29张图片

constructClassInstance 函数中(见上图 1):

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

mountClassInstance

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第30张图片

万字长文+图文并茂+全面解析 React 源码 - render 篇_第31张图片

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

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第32张图片

万字长文+图文并茂+全面解析 React 源码 - render 篇_第33张图片

finishClassComponent

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第34张图片

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第35张图片

万字长文+图文并茂+全面解析 React 源码 - render 篇_第36张图片

React Element(FiberNode) 更新流程 - updateHostComponent

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

React

万字长文+图文并茂+全面解析 React 源码 - render 篇_第37张图片

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

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

创建 真实 DOM 树

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第38张图片

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第39张图片

万字长文+图文并茂+全面解析 React 源码 - render 篇_第40张图片

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第41张图片

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

渲染真实 DOM

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第42张图片

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

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第43张图片

万字长文+图文并茂+全面解析 React 源码 - render 篇_第44张图片

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第45张图片

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

万字长文+图文并茂+全面解析 React 源码 - render 篇_第46张图片

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

原文地址

你可能感兴趣的:(前端成长之路)