React 是笔者的主要技术栈之一,难免想探索一下内部的一些机制实现,笔者会从组件的初始化与挂载、任务调度、组件类型和生命周期、事务、事件、hook等方向进行学习解析(基于 16.8.6 版本),也是第一次进行源码解析的写作,如有欠妥之处,欢迎朋友们不吝指正和讨论,共同学习进步。
下面,我们先来看看 React 组件的初始化
一、组件是什么
首先编写一个最简单的组件
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<div>This is Component App</div>
);
}
}
console.log(<App />)
export default App;
这段代码我们实现了 <App />
组件,我们打印出来看看是个啥:
可以看出,<App />
其实是js对象不是真实的DOM,注意此时的 props
是空对象,接下来我们打印 <App><div>This is Component App</div></App>
:
我们看到,props
发生了变化,由于 <App />
组件中嵌套了一个 div
,div
中又嵌套了文字,所以在描述 <App />
对象的 props
中增加了 children
属性,其值为描述 div
的js对象。同理,如果我们进行多层的组件嵌套,其实就是在父对象的 props
中增加 children
字段及对应的描述值,也就是js对象的多层嵌套。
那么展现在我们眼前的html标签,实际上却是对象的React组件是如何实现的呢?
我们从代码的组件声明为源头进行探索,所以我们打开react内umd规范下的 react.development.js
,看到如下代码:
我们在 import React, { Component } from 'react'
时,引入的就是源码中提供的 React
对象。在 extends Component
时,继承了 Component
类。因为 Component
是 React.Component
的引用。也就是说 Component === React.Component
,在实际项目中都可以写。
沿着 Component
线索我们继续在这个文件里搜寻一下它的踪迹:
上述代码大家再熟悉不过了。同时我们也发现 setState
是定义在原型上具有两个参数的方法,具体原理我们将在后续的篇章进行解析。
上述代码表明,我们在最开始声明的组件App,其实是继承 Component
类,它的原型具有 setState
等方法。这样组件App已经有了最基本的雏形。
二、组件的初始化
声明App后,我们可以在其内部自定义方法,也可以使用生命周期的方法,这些和我们在写"类"的时候是完全一样的。唯一不同的是组件类必须拥有 render
方法输出类似 <div>This is Component App</div>
的结构并挂载到真实DOM上,才能触发组件的生命周期并成为DOM树的一部分。首先我们观察ES6的"类"是如何初始化一个react组件的。
将最初的代码放入 babel 中
其中 _Component
是对象 Component
,_inherit
方法是 extends
关键字的函数实现,这些都是ES6相关内容,我们暂时不管。关键在于我们发现 render
方法实际上是调用了 _react.default.createElement
方法。然后我们继续在我们的源码文件中搜寻 createElement
:
接着搜寻 ReactElement
:
看到这里我们发现,每一个组件对象都是通过 ReactElement
方法创建出来的对象。换句话说,ReactElment
是一种内部记录组件特征的对象。
在 ReactElement
中:
?typeof
: 使用符号标记每个ReactElementtype
: 用于判断如何创建节点key
: DOM结构标识,提升update性能ref
: 真实DOM的props
: 子结构相关信息(有则增加children字段/没有为空)和组件属性(如style等等)- _owner: _owner === ReactCurrentOwner.current, 值为创建当前组件的对象,默认值为null
总的来说,ReactElement
就是一个用来承载信息的容器,对于后期构建应用的树结构是非常重要的。
二、组件的挂载
我们都知道可以通过 ReactDOM.render(component, mountNode)
的形式对自定义组件/原生DOM/字符串进行挂载。
看下源码实现,找到react-dom内umd规范下的 react-dom.development.js
,找到 render
方法看一下:
因为是首次渲染更新,所以 root
是 null
,只需看 !root
的情况,它调用了 legacyCreateRootFromDOMContainer
,我们接着往下看:
此时 forceHydrate
是 false
,所以看 shouldHydrateDueToLegacyHeuristic(container)
返回的是啥?
container
的首节点是没有 data-reactroot
属性的,所以会进行while循环,依次删除 container
的子节点,删除完毕后,new
一个 ReactRoot
的实例。所以说判断是否是服务端渲染的标志是:
在获取 container
中的第一个节点(或文档节点)后,看该节点是否有属性 ROOT_ATTRIBUTE_NAME
。
接着 ReactRoot
的线索往下看,通过 createContainer
去创建 FiberRoot
, 这个 FiberRoot
对象在后期调度更新的过程中非常非常重要。
我们发现调用了 updateContainer
方法后,内部开始计算时间 requestCurrentTime()
, computeExpirationForFiber()
暂且先不管具体细节。
enqueueUpdate()
和 scheduleWork()
,这个任务调度是最重要且最复杂的内容了,后续慢慢来解析。以上我们看见这个计划更新root的函数最终返回了一个 expirationTime
, 就是说 updateContainer()
返回了 expirationTime
。
到这先告一段落了,我们接着回到 legacyRenderSubtreeIntoContainer
函数往下看
最终返回了 getPublicRootInstance
:
React初始化的时候,container.current
是没有子节点的,所以返回 null。
最后我们对之前的思维图再进行补充:
已经有点长了,暂时先解析到这里。组件是如何挂载到真正的DOM上以及它的任务调度又是如何,笔者在下一篇再来解析。