这是个人 React 源码解读课的第一篇文章,首先来讲说为啥要写这个系列文章:html
这个系列文章预计篇数会超过十篇,React 版本为 16.8.6,如下是本系列文章你必须须要注意的地方:前端
这篇文章内容不会很难,先给你们热个身,请你们打开 个人代码 并定位到 react 文件夹下的 src,这个文件夹也就是 React 的入口文件夹了。react
开始进入正文前先说下这个系列中个人行文思路:1. 代码尽可能经过图片展现,既美观又方便阅读,反正不须要你们复制代码。2. 文章中只会讲我认为重要或者有意思的代码,对于其余代码请自行阅读个人仓库,反正已经注释好代码了。3. 对于流程长的函数调用会使用流程图的方式来总结。4. 不会干巴巴的只讲代码,会结合实际来聊聊这些 API 能帮助咱们解决什么问题。 git
你们在写 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
并把内建的几个属性(好比 ref
和 key
)剔除后丢到 props 对象中。dom
接下里是一段对于 children
的操做
首先把第二个参数以后的参数取出来,而后判断长度是否大于一。大于一的话就表明有多个 children
,这时候 props.children
会是一个数组,不然的话只是一个对象。所以咱们须要注意在对 props.children
进行遍历的时候要注意它是不是数组,固然你也能够利用 React.Children
中的 API,下文中也会对 React.Children
中的 API 进行讲解。
最后就是返回了一个 ReactElement
对象
内部代码很简单,核心就是经过 ?typeof
来帮助咱们识别这是一个 ReactElement
,后面咱们能够看到不少这样相似的类型。另外咱们须要注意一点的是:经过 JSX写的 <APP />
表明着 ReactElement
,APP
表明着 React Component。
如下是这一小节的流程图内容:
上文中讲到了 APP
表明着 React Component,那么这一小节咱们就来阅读组件相关也就是 ReactBaseClasses.js 文件下的代码。
其实在阅读这部分源码以前,我觉得代码会很复杂,可能包含了不少组件内的逻辑,结果内部代码至关简单。这是由于 React 团队将复杂的逻辑所有丢在了 react-dom 文件夹中,你能够把 react-dom 当作是 React 和 UI 之间的胶水层,这层胶水能够兼容不少平台,好比 Web、RN、SSR 等等。
该文件包含两个基本组件,分别为 Component
及 PureComponent
,咱们先来阅读 Component
这部分的代码。
构造函数 Component
中须要注意的两点分别是 refs
和 updater
,前者会在下文中专门介绍,后者是组件中至关重要的一个属性,咱们能够发现 setState
和 forceUpdate
都是调用了 updater
中的方法,可是 updater
是 react-dom 中的内容,咱们会在以后的文章中学习到这部分的内容。
另外 ReactNoopUpdateQueue
也有一个单独的文件,可是内部的代码看不看都无所谓,由于都是用于报警告的。
接下来咱们来阅读 PureComponent
中的代码,其实这部分的代码基本与 Component
一致
PureComponent
继承自 Component
,继承方法使用了很典型的寄生组合式。
另外这两部分代码你能够发现每一个组件都有一个 isXXXX
属性用来标志自身属于什么组件。
以上就是这部分的代码,接下来的一小节咱们将会学习到 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> )) 复制代码
这一小节会是这篇文章中最复杂的一部分,可能须要本身写个 Demo 而且 Debug 一下才能真正理解源码为何要这样实现。
首先你们须要定位到 ReactChildren.js 文件,这部分代码中我只会介绍关于 mapChildren
函数相关的内容,由于这部分代码基本就贯穿了整个文件了。
若是你没有使用过这个 API,能够先自行阅读 文档。
对于 mapChildren
这个函数来讲,一般会使用在组合组件设计模式上。若是你不清楚什么是组合组件的话,能够看下 Ant-design,它内部大量使用了这种设计模式,好比说 Radio.Group
、Radio.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
内部究竟是如何实现的。
这段代码有意思的部分是引入了对象重用池的概念,分别对应 getPooledTraverseContext
和 releaseTraverseContext
中的代码。固然这个概念的用处其实很简单,就是维护一个大小固定的对象重用池,每次从这个池子里取一个对象去赋值,用完了就将对象上的属性置空而后丢回池子。维护这个池子的用意就是提升性能,毕竟频繁建立销毁一个有不少属性的对象会消耗性能。
接下来咱们来学习 traverseAllChildrenImpl
中的代码,这部分的代码须要分为两块来说
这部分的代码相对来讲简单点,主体就是在判断 children
的类型是什么。若是是能够渲染的节点的话,就直接调用 callback
,另外你还能够发如今判断的过程当中,代码中有使用到 ?typeof
去判断的流程。这里的 callback
指的是 mapSingleChildIntoContext
函数,这部分的内容会在下文中说到。
这部分的代码首先会判断 children
是否为数组。若是为数组的话,就遍历数组并把其中的每一个元素都递归调用 traverseAllChildrenImpl
,也就是说必须是单个可渲染节点才能够执行上半部分代码中的 callback
。
若是不是数组的话,就看看 children
是否能够支持迭代,原理就是经过 obj[Symbol.iterator]
的方式去取迭代器,返回值若是是个函数的话就表明支持迭代,而后逻辑就和以前的同样了。
讲完了 traverseAllChildrenImpl
函数,咱们最后再来阅读下 mapSingleChildIntoContext
函数中的实现。
bookKeeping
就是咱们从对象池子里取出来的东西,而后调用 func
而且传入节点(此时这个节点确定是单个节点),此时的 func
表明着 React.mapChildren
中的第二个参数。
接下来就是判断返回值类型的过程:若是是数组的话,仍是回归以前的代码逻辑,注意这里传入的 func
是 c => c
,由于要保证最终结果是被摊平的;若是不是数组的话,判断返回值是不是一个有效的 Element,验证经过的话就 clone 一份而且替换掉 key
,最后把返回值放入 result
中,result
其实也就是 mapChildren
的返回值。
至此,mapChildren
函数相关的内容已经解析完毕,还不怎么清楚的同窗能够经过如下的流程图再复习一遍。
前面几小节的内容已经把 react 文件夹下大部分有意思的代码都讲完了,其余就剩余了一些边边角角的内容。好比 memo
、context
、hooks
、lazy
,这部分代码有兴趣的能够直接自行阅读,反正内容都仍是很简单的,难的部分都在 react-dom 文件夹中。
阅读源码是一个很枯燥的过程,可是收益也是巨大的。若是你在阅读的过程当中有任何的问题,都欢迎你在评论区与我交流,固然你也能够在仓库中提 Issus。
另外写这系列是个很耗时的工程,须要维护代码注释,还得把文章写得尽可能让读者看懂,最后还得配上画图,若是你以为文章看着还行,就请不要吝啬你的点赞。
下一篇文章就会是 Fiber 相关的内容,而且会分红几篇文章来说解。
最后,以为内容有帮助能够关注下个人公众号 「前端真好玩」咯,会有不少好东西等着你。