1. 首页

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

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

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

1.组件是什么

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

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

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

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

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

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

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

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

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

因为我们的组件声明基于ReactComponent,所以首先我们打开React.js,可以看到如下代码:

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

我们在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:

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

上述代码是再熟悉不过的构造函数,想必大家已经滚瓜烂熟了。同时我们也注意到setState是定义在原型上具有两个参数的方法,具体原理我们将在 React 更新机制的篇章讲解。

上述代码表明,我们在最开始声明的组件 A,其实是继承ReactComponent类的子类,它的原型具有setState等方法。这样组件 A 已经有了最基本的雏形。

小结

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

2.组件的初始化

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

将最初的示例代码放入 babel 中:

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

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

Js 中文网 – 前端进阶资源教程 https://www.javascriptC.com/,typescript 中文手册
专注分享前端知识,你想要的,在这里都能找到

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

看到这里我们发现,其实每一个组件对象都是通过React.createElement方法创建出来的ReactElement类型的对象。换句话说,ReactElment是一种内部记录组件特征并告诉 React 你想在屏幕上看到什么的对象。 在ReactElement中:

参数功能
$$typeof组件的标识信息
keyDOM结构标识,提升update性能
props子结构相关信息(有则增加children字段/没有为空)和组件属性(如style)
ref真实DOM的引用
_owner_owner === ReactCurrentOwner.current(ReactCurrentOwner.js),值为创建当前组件的对象,默认值为null。

看完上述内容相信大家已经对React组件的实质有了一定的了解。通过执行React.createElement创建出的ReactElement类型的js对象,就是”React组件”,这与控制台打印出的结果完全对应。总结来说,如果我们通过class关键字声明React组件,那么他们在解析成真实DOM之前一直是ReactElement类型的js对象。

小结

对之前的思维导图进行补充:

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

Js 中文网 – 前端进阶资源教程 https://www.javascriptC.com/,typescript 中文手册
专注分享前端知识,你想要的,在这里都能找到

3.组件的挂载

我们知道可以通过ReactDOM.render(component,mountNode)的形式对自定义组件/原生 DOM/字符串进行挂载,

那么挂载的过程又是如何实现的呢?

ReactDOM.render实际调用了内部的ReactMount.render,进而执行ReactMount._renderSubtreeIntoContainer。从字面意思上就可以看出是将”子 DOM”插入容器的逻辑,我们看下源码实现:

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

这段代码非常重要,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的源码:

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

分析一下流程:

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

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

核心代码就是最后两行。setInnerHTML是一个方法,将markup设置为containerinnerHTML属性,这样就完成了 DOM 的插入。precacheNode方法是将处理好的组件对象存储在缓存中,提高结构更新的速度。

React 组件初始化和挂载的流程到这里基本明朗了。在ReactDOM.render()的方法使用中,我们会注意到该方法可以挂载 React 组件,也可以挂载字符串,也可以挂载原生 DOM。现在我们已经知道,其实挂载就是利用innerHTML属性,但是对于不同的元素结构,React 是否也有不同的处理呢?

上文我们提到,在组件挂载的倒数第二步,也就是执行_renderNewRootComponent方法时,我们看到有一个名为instantiateReactComponent的方法返回一个经过加工的对象。我们看下instantiateReactComponent的源码:

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

传入的参数node就是ReactDOM.render方法的组件参数,输入node和输出instance可以总结如下表:

node实际参数结果
null/false创建ReactEmptyComponent组件
object && type === string虚拟DOM创建ReactDOMComponent组件
object && type !== stringReact组件创建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 源码解析(三):详解事务与更新队列》 > 《React 源码解析(四):事件系统》
联系邮箱:ssssyoki@foxmail.com

作者:ssssyoki
链接:https://juejin.im/post/5983dfbcf265da3e2f7f32de

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

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

链接:https://www.javascriptc.com/3951.html

« 前端快速建⽴Mock App
撸一个前端监控系统(React + Node + Mysql + Webpack plugin + Docker)—— (上)»
Flutter 中文教程资源

相关推荐

QR code