剖析 React 源码:render 流程(一)

这是个人剖析 React 源码的第二篇文章,若是你没有阅读过以前的文章,请务必先阅读一下 第一篇文章 中提到的一些注意事项,能帮助你更好地阅读源码。html

文章相关资料

如今请你们打开 个人代码 并定位到 react-dom 文件夹下的 src 中的 ReactDOM.js 文件,今天的内容会从这里开始。前端

render

想必你们在写 React 项目的时候都写过相似的代码react

ReactDOM.render(<APP />, document.getElementById('root') 复制代码

这句代码告诉了 React 应用咱们想在容器中渲染出一个组件,这一般也是一个 React 应用的入口代码,接下来咱们就来梳理整个 render 的流程,而且会分为几篇文章来说解,由于流程实在太长了。git

首先请你们先定位到 ReactDOM.js 文件的第 702 行代码,开始今天的旅程。github

这部分代码其实没啥好说的,惟一须要注意的是在调用 legacyRenderSubtreeIntoContainer 函数时写死了第四个参数 forceHydratefalse。这个参数为 true 时代表了是服务端渲染,由于咱们分析的是客户端渲染,所以后面有关这部分的内容也不会再展开。性能优化

接下来进入 legacyRenderSubtreeIntoContainer 函数中,这部分代码分为两块来说。第一部分是没有 root 以前咱们首先须要建立一个 root(对应这篇文章),第二部分是有 root 以后的渲染流程(对应接下来的文章)。markdown

一开始进来函数的时候确定是没有 root 的,所以咱们须要去建立一个 root,你们能够发现这个 root 对象一样也被挂载在了 container._reactRootContainer 上,也就是咱们的 DOM 容器上。 若是你手边有 React 项目的话,在控制台键入以下代码就能够看到这个 root 对象了。数据结构

document.querySelector('#root')._reactRootContainer
复制代码

你们能够看到 rootReactRoot 构造函数构造出来的,而且内部有一个 _internalRoot 对象,这个对象是本文接下来要重点介绍的 fiber 对象,接下来咱们就来一窥究竟吧。架构

首先仍是和上文中提到的 forceHydrate 属性相关的内容,不须要管这部分,反正 shouldHydrate 确定为 falsedom

接下来是将容器内部的节点所有移除,通常来讲咱们都是这样写一个容器的的

<div id='root'></div>
复制代码

这样的形式确定就不须要去移除子节点了,这也侧面说明了一点那就是容器内部不要含有任何的子节点。一是确定会被移除掉,二来还要进行 DOM 操做,可能还会涉及到重绘回流等等。

最后就是建立了一个 ReactRoot 对象并返回。接下来的内容中咱们会看到好几个 root,可能会有点绕。

ReactRoot 构造函数内部就进行了一步操做,那就是建立了一个 FiberRoot 对象,并挂载到了 _internalRoot 上。和 DOM 树同样,fiber 也会构建出一个树结构(每一个 DOM 节点必定对应着一个 fiber 对象),FiberRoot 就是整个 fiber 树的根节点,接下来的内容里咱们将学习到关于 fiber 相关的内容。这里说起一点,fiber 和 Fiber 是两个不同的东西,前者表明着数据结构,后者表明着新的架构。

createFiberRoot 函数内部,分别建立了两个 root,一个 root 叫作 FiberRoot,另外一个 root 叫作 RootFiber,而且它们二者仍是相互引用的。

这两个对象内部拥有着数十个属性,如今咱们没有必要一一去了解它们各自有什么用处,在当下只须要了解少部分属性便可,其余的属性咱们会在之后的文章中了解到它们的用处。

对于 FiberRoot 对象来讲,咱们如今只须要了解两个属性,分别是 containerInfocurrent。前者表明着容器信息,也就是咱们的 document.querySelector('#root');后者指向 RootFiber

对于 RootFiber 对象来讲,咱们须要了解的属性稍微多点

function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) {
  this.stateNode = null;
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.effectTag = NoEffect;
  this.alternate = null;
}
复制代码

stateNode 上文中已经讲过了,这里就再也不赘述。

returnchildsibling 这三个属性很重要,它们是构成 fiber 树的主体数据结构。fiber 树实际上是一个单链表树结构,returnchild 分别对应着树的父子节点,而且父节点只有一个 child 指向它的第一个子节点,即使是父节点有好多个子节点。那么多个子节点如何链接起来呢?答案是 sibling,每一个子节点都有一个 sibling 属性指向着下一个子节点,都有一个 return 属性指向着父节点。这么说可能有点绕,咱们经过图来了解一下这个 fiber 树的结构。

const APP = () => (
    <div> <span></span> <span></span> </div>
)
ReactDom.render(<APP/>, document.querySelector('#root'))
复制代码

假如说咱们须要渲染出以上组件,那么它们对应的 fiber 树应该长这样

从图中咱们能够看到,每一个组件或者 DOM 节点都会对应着一个 fiber 对象。另外你手边有 React 项目的话,也能够在控制台输入以下代码,查看 fiber 树的整个结构。

// 对应着 FiberRoot
const fiber = document.querySelector('#root')._reactRootContainer._internalRoot
复制代码

另外两个属性在本文中虽然用不上,可是看源码的时候笔者以为颇有意思,就打算拿出来讲一下。

在说 effectTag 以前,咱们先来了解下啥是 effect,简单来讲就是 DOM 的一些操做,好比增删改,那么 effectTag 就是来记录全部的 effect 的,可是这个记录是经过位运算来实现的,这里effectTag 相关的二进制内容。

若是咱们想新增一个 effect 的话,能够这样写 effectTag |= Update;若是咱们想删除一个 effect 的话,能够这样写 effectTag &= ~Update

最后是 alternate 属性。其实在一个 React 应用中,一般来讲都有两个 fiebr 树,一个叫作 old tree,另外一个叫作 workInProgress tree。前者对应着已经渲染好的 DOM 树,后者是正在执行更新中的 fiber tree,还能便于中断后恢复。两棵树的节点互相引用,便于共享一些内部的属性,减小内存的开销。毕竟前文说过每一个组件或 DOM 都会对应着一个 fiber 对象,应用很大的话组成的 fiber 树也会很大,若是两棵树都是各自把一些相同的属性建立一遍的话,会损失很多的内存空间及性能。

当更新结束之后,workInProgress tree 会将 old tree 替换掉,这种作法称之为 double buffering,这也是性能优化里的一种作法,有兴趣的同窗能够自行查找资料。

总结

以上就是本文的所有内容了,最后经过一张流程图总结一下这篇文章的内容。

最后

阅读源码是一个很枯燥的过程,可是收益也是巨大的。若是你在阅读的过程当中有任何的问题,都欢迎你在评论区与我交流。

另外写这系列是个很耗时的工程,须要维护代码注释,还得把文章写得尽可能让读者看懂,最后还得配上画图,若是你以为文章看着还行,就请不要吝啬你的点赞。

下一篇文章仍是 render 流程相关的内容。

最后,以为内容有帮助能够关注下个人公众号 「前端真好玩」咯,会有不少好东西等着你。

相关文章
相关标签/搜索