剖析 React 源码:先热个身

这是个人 React 源码解读课的第一篇文章,首先来讲说为啥要写这个系列文章:html

  • 如今工做中基本都用 React 了,由此想了解下内部原理
  • 市面上 Vue 的源码解读数不胜数,可是反观 React 相关的却寥寥无几,也是由于 React 源码难度较高,所以我想来攻克这个难题
  • 本身以为看懂并不必定看懂了,写出来让读者看懂才是真懂了,所以我要把我读懂的东西写出来

这个系列文章预计篇数会超过十篇,React 版本为 16.8.6,如下是本系列文章你必须须要注意的地方:前端

  • 这是一门进阶课,若是涉及到你不清楚的内容,请自行谷歌,另外最好具有 React 的开发能力
  • 这是一门讲源码的课,只阅读是不大可能真正读懂的,须要辅以 Demo 和 Debug 才能真正理解代码的用途
  • 我 fork 了一份 16.8.6 版本的代码,而且会为读过的代码加上详细的中文注释。等不及我文章的同窗能够先行阅读 个人仓库而且在阅读本系列文章的时候也请跟着阅读我注释的代码。由于版本不一样可能会致使代码不一样,而且我不会在文章中贴上大段的代码,只会对部分代码作更详细的解释,其余的代码能够跟着个人注释阅读
  • 阅读源码最早遇到的问题会是不知道该从何开始,我这份代码注释能够帮助你们解决这个问题,你只须要跟着个人 commit 阅读便可
  • 不会对任何 DEV 环境下的代码作解读,不会对全部代码进行解读,只会解读核心功能(即便这样也会是一个大工程)
  • 最后再说起一遍,请务必文章和 代码 相结合来看,为了篇幅考虑我不会将全部的代码都贴上来,我拷贝的累,读者看的也累

这篇文章内容不会很难,先给你们热个身,请你们打开 个人代码 并定位到 react 文件夹下的 src,这个文件夹也就是 React 的入口文件夹了。react

开始进入正文前先说下这个系列中个人行文思路:1. 代码尽可能经过图片展现,既美观又方便阅读,反正不须要你们复制代码。2. 文章中只会讲我认为重要或者有意思的代码,对于其余代码请自行阅读个人仓库,反正已经注释好代码了。3. 对于流程长的函数调用会使用流程图的方式来总结。4. 不会干巴巴的只讲代码,会结合实际来聊聊这些 API 能帮助咱们解决什么问题。 git

文章相关资料

React.createElement

你们在写 React 代码的时候确定写过 JSX,可是为何一旦使用 JSX 就必须引入 React 呢?github

这是由于咱们的 JSX 代码会被 Babel 编译为 React.createElement,不引入 React 的话就不能使用 React.createElement 了。设计模式

<div id='1'>1</div>
// 上面的 JSX 会被编译成这样
React.createElement("div", {
  id: "1"
}, "1")
复制代码

那么咱们就先定位到 ReactElement.js 文件阅读下 createElement 函数的实现api

export function createElement(type, config, children) {}
复制代码

首先 createElement 函数接收三个参数,具体表明着什么相信你们能够经过上面 JSX 编译出来的东西自行理解。数组

而后是对于 config 的一些处理:markdown

这段代码对 ref 以及 key 作了个验证(对于这种代码就无须阅读内部实现,经过函数名就能够了解它想作的事情),而后遍历 config 并把内建的几个属性(好比 refkey)剔除后丢到 props 对象中。dom

接下里是一段对于 children 的操做

首先把第二个参数以后的参数取出来,而后判断长度是否大于一。大于一的话就表明有多个 children,这时候 props.children 会是一个数组,不然的话只是一个对象。所以咱们须要注意在对 props.children 进行遍历的时候要注意它是不是数组,固然你也能够利用 React.Children 中的 API,下文中也会对 React.Children 中的 API 进行讲解。

最后就是返回了一个 ReactElement 对象

内部代码很简单,核心就是经过 ?typeof 来帮助咱们识别这是一个 ReactElement,后面咱们能够看到不少这样相似的类型。另外咱们须要注意一点的是:经过 JSX写的 <APP /> 表明着 ReactElementAPP 表明着 React Component。

如下是这一小节的流程图内容:

ReactBaseClasses

上文中讲到了 APP 表明着 React Component,那么这一小节咱们就来阅读组件相关也就是 ReactBaseClasses.js 文件下的代码。

其实在阅读这部分源码以前,我觉得代码会很复杂,可能包含了不少组件内的逻辑,结果内部代码至关简单。这是由于 React 团队将复杂的逻辑所有丢在了 react-dom 文件夹中,你能够把 react-dom 当作是 React 和 UI 之间的胶水层,这层胶水能够兼容不少平台,好比 Web、RN、SSR 等等。

该文件包含两个基本组件,分别为 ComponentPureComponent,咱们先来阅读 Component 这部分的代码。

构造函数 Component 中须要注意的两点分别是 refsupdater,前者会在下文中专门介绍,后者是组件中至关重要的一个属性,咱们能够发现 setStateforceUpdate 都是调用了 updater 中的方法,可是 updater 是 react-dom 中的内容,咱们会在以后的文章中学习到这部分的内容。

另外 ReactNoopUpdateQueue 也有一个单独的文件,可是内部的代码看不看都无所谓,由于都是用于报警告的。

接下来咱们来阅读 PureComponent 中的代码,其实这部分的代码基本与 Component 一致

PureComponent 继承自 Component,继承方法使用了很典型的寄生组合式。

另外这两部分代码你能够发现每一个组件都有一个 isXXXX 属性用来标志自身属于什么组件。

以上就是这部分的代码,接下来的一小节咱们将会学习到 refs 的一部份内容。

Refs

refs 其实有好几种方式能够建立:

  • 字符串的方式,可是这种方式已经不推荐使用
  • ref={el => this.el = el}
  • React.createRef

这一小节咱们来学习 React.createRef 相关的内容,其他的两种方式不在这篇文章的讨论范围以内,请先定位到 ReactCreateRef.js 文件。

内部实现很简单,若是咱们想使用 ref,只须要取出其中的 current 对象便可。

另外对于函数组件来讲,是不能使用 ref 的,若是你不知道缘由的话能够直接阅读 文档

固然在以前也是有取巧的方式的,就是经过 props 的方式传递 ref,可是如今咱们有了新的方式 forwardRef 去解决这个问题。

具体代码见 forwardRef.js 文件,一样内部代码仍是很简单

这部分代码最重要的就是咱们能够在参数中得到 ref 了,所以咱们若是想在函数组件中使用 ref 的话就能够把代码写成这样:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton"> {props.children} </button>
))
复制代码

ReactChildren

这一小节会是这篇文章中最复杂的一部分,可能须要本身写个 Demo 而且 Debug 一下才能真正理解源码为何要这样实现。

首先你们须要定位到 ReactChildren.js 文件,这部分代码中我只会介绍关于 mapChildren 函数相关的内容,由于这部分代码基本就贯穿了整个文件了。

若是你没有使用过这个 API,能够先自行阅读 文档

对于 mapChildren 这个函数来讲,一般会使用在组合组件设计模式上。若是你不清楚什么是组合组件的话,能够看下 Ant-design,它内部大量使用了这种设计模式,好比说 Radio.GroupRadio.Button,另外这里也有篇 文档 介绍了这种设计模式。

咱们先来看下这个函数的一些神奇用法

React.Children.map(this.props.children, c => [[c, c]])
复制代码

对于上述代码,map 也就是 mapChildren 函数来讲返回值是 [c, c, c, c]。无论你第二个参数的函数返回值是几维嵌套数组,map 函数都能帮你摊平到一维数组,而且每次遍历后返回的数组中的元素个数表明了同一个节点须要复制几回。

若是文字描述有点难懂的话,就来看代码吧:

<div>
    <span>1</span>
    <span>2</span>
</div>
复制代码

对于上述代码来讲,经过 c => [[c, c]] 转换之后就变成了

<span>1</span>
<span>1</span>
<span>2</span>
<span>2</span>
复制代码

接下里咱们进入正题,来看看 mapChildren 内部究竟是如何实现的。

这段代码有意思的部分是引入了对象重用池的概念,分别对应 getPooledTraverseContextreleaseTraverseContext 中的代码。固然这个概念的用处其实很简单,就是维护一个大小固定的对象重用池,每次从这个池子里取一个对象去赋值,用完了就将对象上的属性置空而后丢回池子。维护这个池子的用意就是提升性能,毕竟频繁建立销毁一个有不少属性的对象会消耗性能。

接下来咱们来学习 traverseAllChildrenImpl 中的代码,这部分的代码须要分为两块来说

这部分的代码相对来讲简单点,主体就是在判断 children 的类型是什么。若是是能够渲染的节点的话,就直接调用 callback,另外你还能够发如今判断的过程当中,代码中有使用到 ?typeof 去判断的流程。这里的 callback 指的是 mapSingleChildIntoContext 函数,这部分的内容会在下文中说到。

这部分的代码首先会判断 children 是否为数组。若是为数组的话,就遍历数组并把其中的每一个元素都递归调用 traverseAllChildrenImpl,也就是说必须是单个可渲染节点才能够执行上半部分代码中的 callback

若是不是数组的话,就看看 children 是否能够支持迭代,原理就是经过 obj[Symbol.iterator] 的方式去取迭代器,返回值若是是个函数的话就表明支持迭代,而后逻辑就和以前的同样了。

讲完了 traverseAllChildrenImpl 函数,咱们最后再来阅读下 mapSingleChildIntoContext 函数中的实现。

bookKeeping 就是咱们从对象池子里取出来的东西,而后调用 func 而且传入节点(此时这个节点确定是单个节点),此时的 func 表明着 React.mapChildren 中的第二个参数。

接下来就是判断返回值类型的过程:若是是数组的话,仍是回归以前的代码逻辑,注意这里传入的 funcc => c,由于要保证最终结果是被摊平的;若是不是数组的话,判断返回值是不是一个有效的 Element,验证经过的话就 clone 一份而且替换掉 key,最后把返回值放入 result 中,result 其实也就是 mapChildren 的返回值。

至此,mapChildren 函数相关的内容已经解析完毕,还不怎么清楚的同窗能够经过如下的流程图再复习一遍。

其他内容

前面几小节的内容已经把 react 文件夹下大部分有意思的代码都讲完了,其余就剩余了一些边边角角的内容。好比 memocontexthookslazy,这部分代码有兴趣的能够直接自行阅读,反正内容都仍是很简单的,难的部分都在 react-dom 文件夹中。

其余文章列表

最后

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

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

下一篇文章就会是 Fiber 相关的内容,而且会分红几篇文章来说解。

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

相关文章
相关标签/搜索