React源码解析(一):组件的实现与挂载
当我们能够熟练运用React进行前端开发时,不免会对React内部机制产生浓厚的兴趣。组件是什么?是真的DOM吗?生命周期函数的执行依据又是什么呢?
本篇,我们先来研究React组件的实现与挂载。
1.组件是什么
首先编写一个最简单的组件:

上述代码写完后,我们就得到了<A />
这个组件,那么我们接下来先弄清楚<A />
是什么。用console.log
打印出来:

可以看出,<A />
其实是js对象而不是真实的DOM,注意此时props
是空对象。接下来,我们打印<A><div>这是A组件</div></A>
,看看控制台会输出什么:

我们看到,props
发生了变化,由于<A />
组件中嵌套了一个div
,div
中又嵌套了文字,所以在描述<A />
对象的props
中增加了children
属性,其值为描述div
的js对象。同理,如果我们进行多层的组件嵌套,其实就是在父对象的props
中增加children
字段及对应的描述值,也就是js对象的多层嵌套。
以上描述是基于ES6的React开发模式,其实在ES5中通过React.createClass({})
方法创建的组件,与ES6中是完全一样的,同样可以通过控制台打印输出组件结果进行验证,此处不再赘述。
那么形如HTML标签实际上却是对象的React组件是如何构成的呢?
因为我们的组件声明基于React
和Component
,所以首先我们打开React.js
,可以看到如下代码:

我们在import React from 'react'
时,引入的就是源码中提供的React对象。在extends Component
时,继承了Component
类。这里需要说明两点:
- 源码中明明使用的
module.exports
而不是export default
,为什么还能够成功引入呢?其实这是babel解析器的功劳。它令(ES6)import === (CommonJS)require
。而在typescript中,需要严格的export default
声明,故在typescript下就不能使用import React from 'react'
了,有兴趣的读者可以尝试一下。 - 我们可以写
extends Component
也可以写extends React.Component
,这两者是否存在区别呢?答案是否定的。因为Component
是React.Component
的引用。也就是说Component === React.Component
,在实际项目中写哪个都可以。
沿着ReactComponent
的线索,我们打开node_modules/react/lib/ReactComponent.js
:

上述代码是再熟悉不过的构造函数,想必大家已经滚瓜烂熟了。同时我们也注意到setState
是定义在原型上具有两个参数的方法,具体原理我们将在React更新机制的篇章讲解。
上述代码表明,我们在最开始声明的组件A,其实是继承ReactComponent
类的子类,它的原型具有setState
等方法。这样组件A已经有了最基本的雏形。
小结

2.组件的初始化
声明A后,我们可以在其内部自定义方法,也可以使用生命周期的方法,如ComponentDidMount
等等,这些和我们在写"类"的时候是完全一样的。唯一不同的是组件类必须拥有render
方法输出类似<div>这是A组件</div>
的结构并挂载到真实DOM上,才能触发组件的生命周期并成为DOM树的一部分。首先我们观察ES6的"类"是如何初始化一个react组件的。
将最初的示例代码放入babel中:

其中_Component
是对象ReactComponent
,_inherit
方法是extends
关键字的函数实现,这些都是ES6相关内容,我们暂时不管。关键在于我们发现render
方法实际上是调用了React.createElement
方法(实际是ReactElement方法)。然后我们打开ReactElement.js
:

看到这里我们发现,其实每一个组件对象都是通过React.createElement
方法创建出来的ReactElement
类型的对象。换句话说,ReactElment
是一种内部记录组件特征并告诉React你想在屏幕上看到什么的对象。
在ReactElement
中:
参数 | 功能 |
---|---|
?typeof |
组件的标识信息 |
key |
DOM结构标识,提升update性能 |
props |
子结构相关信息(有则增加children 字段/没有为空)和组件属性(如style ) |
ref |
真实DOM的引用 |
_owner |
_owner === ReactCurrentOwner.current (ReactCurrentOwner.js),值为创建当前组件的对象,默认值为null。 |
看完上述内容相信大家已经对React组件的实质有了一定的了解。通过执行React.createElement
创建出的ReactElement
类型的js对象,就是"React组件",这与控制台打印出的结果完全对应。总结来说,如果我们通过class
关键字声明React组件,那么他们在解析成真实DOM之前一直是ReactElement
类型的js对象。
小结
对之前的思维导图进行补充:

3.组件的挂载
我们知道可以通过ReactDOM.render(component,mountNode)
的形式对自定义组件/原生DOM/字符串进行挂载,
那么挂载的过程又是如何实现的呢?
ReactDOM.render
实际调用了内部的ReactMount.render
,进而执行ReactMount._renderSubtreeIntoContainer
。从字面意思上就可以看出是将"子DOM"插入容器的逻辑,我们看下源码实现:

这段代码非常重要,render
函数的功能全部再在此(可点击图片大图)。
我们先来解析传入_renderSubtreeIntoContainer
的参数:
参数 | 功能 |
---|---|
parentComponent |
当前组件的父组件,第一次渲染时为null |
nextElement |
要插入DOM中的组件,如helloWorld |
container |
要插入的容器,如document.getElementById('root') |
callback |
完成后的回调函数 |
这几个参数的功能很好理解,接下来我们逐行进行逻辑分析:
- line 2:将当前组件添加到前一级的
props
属性下。(本文开头已说明父子嵌套关系由props
提供) - line 4 ~ 22:调用
getTopLevelWrapperInContainer
方法判断当前容器下是否存在组件,记为prevComponent
;如果有即prevComponent
为true
,执行更新流程,即调用_updateRootComponent
方法。若不存在,则卸载。(调用unmountComponentAtNode
方法) - line 24:不管是更新还是卸载,最终都要挂载到真实的DOM上。看下
._renderNewRootComponent
的源码:

分析一下流程:
- 第3行出现了
instantiateReactComponent
包装方法,这个我们后面再说。 - 第5行中
batchedMountComponentIntoNode
以事务的形式调用mountComponentIntoNode
(事务将专门拿出一篇文章来解析),该方法返回组件对应的HTML,记为变量markup
。而mountComponentIntoNode
最终调用的是_mountImageIntoNode
,看下源码:

核心代码就是最后两行。setInnerHTML
是一个方法,将markup
设置为container
的innerHTML
属性,这样就完成了DOM的插入。precacheNode
方法是将处理好的组件对象存储在缓存中,提高结构更新的速度。
React组件初始化和挂载的流程到这里基本明朗了。在ReactDOM.render()
的方法使用中,我们会注意到该方法可以挂载React组件,也可以挂载字符串,也可以挂载原生DOM。现在我们已经知道,其实挂载就是利用innerHTML
属性,但是对于不同的元素结构,React是否也有不同的处理呢?
上文我们提到,在组件挂载的倒数第二步,也就是执行_renderNewRootComponent
方法时,我们看到有一个名为instantiateReactComponent
的方法返回一个经过加工的对象。我们看下instantiateReactComponent
的源码:

传入的参数node
就是ReactDOM.render
方法的组件参数,输入node
和输出instance
可以总结如下表:
node |
实际参数 | 结果 |
---|---|---|
null /false |
空 | 创建ReactEmptyComponent 组件 |
object && type === string |
虚拟DOM | 创建ReactDOMComponent 组件 |
object && type !== string |
React组件 | 创建ReactCompositeComponent 组件 |
string |
字符串 | 创建ReactTextComponent 组件 |
number |
数字 | 创建ReactTextComponent 组件 |
梳理一下流程:
- 根据
ReactDOM.render()
传入不同的参数,React内部会创建四大类封装组件,记为componentInstance
。 - 而后将其作为参数传入
mountComponentIntoNode
方法中,由此获得组件对应的HTML,记为变量markup
。 - 将真实的DOM的属性
innerHTML
设置为markup
,即完成了DOM插入。
那么问题来了,在上述第二步是如何解析出HTML的呢?答案是在第一步封装成四大类型组件的过程中,赋予了封装组件mountComponet
方法, 执行该方法会触发组件的生命周期,从而解析出HTML。
当然,这四大类组件我们最常用的就是ReactCompositeComponent
组件,也就是我们常说的React组件,其内部具有完整的生命周期,也是React最关键的组件特性。关于详细的组件类型与生命周期的部分,我们在下一篇文章讲解。
4.总结
用一张图来梳理React组件从声明到初始化再到挂载的流程: (点击可查看大图)

回顾: 《React源码解析(二):组件的类型与生命周期》 《React源码解析(三):详解事务与更新队列》 《React源码解析(四):事件系统》 联系邮箱:ssssyoki@foxmail.com
转载自:https://juejin.cn/post/6844903504528556040