这是个人剖析 React 源码的第二篇文章,若是你没有阅读过以前的文章,请务必先阅读一下 第一篇文章 中提到的一些注意事项,能帮助你更好地阅读源码。html
如今请你们打开 个人代码 并定位到 react-dom 文件夹下的 src 中的 ReactDOM.js 文件,今天的内容会从这里开始。前端
想必你们在写 React 项目的时候都写过相似的代码react
ReactDOM.render(<APP />, document.getElementById('root') 复制代码
这句代码告诉了 React 应用咱们想在容器中渲染出一个组件,这一般也是一个 React 应用的入口代码,接下来咱们就来梳理整个 render
的流程,而且会分为几篇文章来说解,由于流程实在太长了。git
首先请你们先定位到 ReactDOM.js 文件的第 702 行代码,开始今天的旅程。github
这部分代码其实没啥好说的,惟一须要注意的是在调用 legacyRenderSubtreeIntoContainer
函数时写死了第四个参数 forceHydrate
为 false
。这个参数为 true
时代表了是服务端渲染,由于咱们分析的是客户端渲染,所以后面有关这部分的内容也不会再展开。性能优化
接下来进入 legacyRenderSubtreeIntoContainer
函数中,这部分代码分为两块来说。第一部分是没有 root
以前咱们首先须要建立一个 root
(对应这篇文章),第二部分是有 root
以后的渲染流程(对应接下来的文章)。markdown
一开始进来函数的时候确定是没有 root
的,所以咱们须要去建立一个 root
,你们能够发现这个 root
对象一样也被挂载在了 container._reactRootContainer
上,也就是咱们的 DOM 容器上。 若是你手边有 React 项目的话,在控制台键入以下代码就能够看到这个 root
对象了。数据结构
document.querySelector('#root')._reactRootContainer 复制代码
你们能够看到 root
是 ReactRoot
构造函数构造出来的,而且内部有一个 _internalRoot
对象,这个对象是本文接下来要重点介绍的 fiber
对象,接下来咱们就来一窥究竟吧。架构
首先仍是和上文中提到的 forceHydrate
属性相关的内容,不须要管这部分,反正 shouldHydrate
确定为 false
。dom
接下来是将容器内部的节点所有移除,通常来讲咱们都是这样写一个容器的的
<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
对象来讲,咱们如今只须要了解两个属性,分别是 containerInfo
及 current
。前者表明着容器信息,也就是咱们的 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
上文中已经讲过了,这里就再也不赘述。
return
、child
、sibling
这三个属性很重要,它们是构成 fiber
树的主体数据结构。fiber
树实际上是一个单链表树结构,return
及 child
分别对应着树的父子节点,而且父节点只有一个 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 流程相关的内容。
最后,以为内容有帮助能够关注下个人公众号 「前端真好玩」咯,会有不少好东西等着你。