React源码解析(一):组件的实现与挂载

62,779 阅读7分钟

当我们能够熟练运用React进行前端开发时,不免会对React内部机制产生浓厚的兴趣。组件是什么?是真的DOM吗?生命周期函数的执行依据又是什么呢?

本篇,我们先来研究React组件的实现与挂载。

1.组件是什么

首先编写一个最简单的组件:

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

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

我们看到,props发生了变化,由于<A />组件中嵌套了一个divdiv中又嵌套了文字,所以在描述<A />对象的props中增加了children属性,其值为描述div的js对象。同理,如果我们进行多层的组件嵌套,其实就是在父对象的props中增加children字段及对应的描述值,也就是js对象的多层嵌套。

以上描述是基于ES6的React开发模式,其实在ES5中通过React.createClass({})方法创建的组件,与ES6中是完全一样的,同样可以通过控制台打印输出组件结果进行验证,此处不再赘述。

那么形如HTML标签实际上却是对象的React组件是如何构成的呢?

因为我们的组件声明基于ReactComponent,所以首先我们打开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,这两者是否存在区别呢?答案是否定的。因为ComponentReact.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;如果有即prevComponenttrue,执行更新流程,即调用_updateRootComponent方法。若不存在,则卸载。(调用unmountComponentAtNode方法)
  • line 24:不管是更新还是卸载,最终都要挂载到真实的DOM上。看下._renderNewRootComponent的源码:

分析一下流程:

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

核心代码就是最后两行。setInnerHTML是一个方法,将markup设置为containerinnerHTML属性,这样就完成了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