我在学习过程中喜欢做记录,分享的是我在前端之路上的一些积累和思考,也希望能跟大家一起交流与进步。 这是我的 ,欢迎一起学习,欢迎star
本次分析的源码采用的是16.2.0
的版本 目前网上现有的react源码分析文章基于的都是版本16以前的源码,入口和核心构造器不一样了,如下图所示
本想借鉴前人的源码分析成果,奈何完全对不上号,只好自己慢慢摸索
水平有限,如果有错误和疏忽的地方,还请指正。
最快捷开始分析源码的办法
mkdir analyze-react@16.2.0cd analyze-react@16.2.0npm init -ynpm i react --save复制代码
然后打开项目,进入node_nodules => react
先看入口文件ndex.js
'use strict';if (process.env.NODE_ENV === 'production') { module.exports = require('./cjs/react.production.min.js');} else { module.exports = require('./cjs/react.development.js');}复制代码
看开发环境下的版本即可,压缩版本是打包到生产环境用的
打开图中文件
核心接口
分析源码先找对外的暴露接口,当然就是react
了,直接拉到最下面
var React = { Children: { map: mapChildren, forEach: forEachChildren, count: countChildren, toArray: toArray, only: onlyChild }, Component: Component, PureComponent: PureComponent, unstable_AsyncComponent: AsyncComponent, Fragment: REACT_FRAGMENT_TYPE, createElement: createElementWithValidation, cloneElement: cloneElementWithValidation, createFactory: createFactoryWithValidation, isValidElement: isValidElement, version: ReactVersion,};复制代码
ReactChildren
ReactChildren
提供了处理 this.props.children
的工具集,跟旧版本的一样
Children: { map: mapChildren, forEach: forEachChildren, count: countChildren, toArray: toArray, only: onlyChild },复制代码
组件
旧版本只有ReactComponent一种
新版本定义了三种不同类型的组件基类Component
,PureComponent
,unstable_AsyncComponent
Component: Component,PureComponent: PureComponent,unstable_AsyncComponent: AsyncComponent,复制代码
等下再具体看都是什么
生成组件
createElement: createElementWithValidation,cloneElement: cloneElementWithValidation,createFactory: createFactoryWithValidation,复制代码
判断组件:isValidElement
校验是否是合法元素,只需要校验类型,重点是判断.$$typeof
属性
function isValidElement(object) { return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;}复制代码
_assign
其实是object-assign
,但文中有关键地方用到它,下文会讲
var _assign = require('object-assign');复制代码
React组件的本质
组件本质是对象
不急着看代码,先通过例子看看组件是什么样子的 用creact-react-app
生成一个最简单的react项目 在App.js
文件加点东西,然后打印组件A看一下是什么
npm start复制代码
启动项目看看
其实就是个对象,有很多属性,注意到props
里面, 没有内容
现在给组件A里面加一点内容
componentDidMount() { console.log('组件A',加点内容看看) }复制代码
可以看到,props.children
里面开始嵌套内容了
以我们聪明的程序员的逻辑思维能力来推理一下,其实不断的页面嵌套,就是不断的给这个对象嵌套props
而已
不信再看一下
componentDidMount() { console.log('组件A',加点内容看看不信再加多一点) }复制代码
虚拟DOM概念
所以到目前为止,我们知道了react的组件只是对象,而我们都知道真正的页面是由一个一个的DOM节点组成的,在比较原生的jQuery年代,通过JS来操纵DOM元素,而且都是真实的DOM元素,而且我们都知道复杂或频繁的DOM操作通常是性能瓶颈产生的原因, 所以React引入了虚拟DOM(Virtual DOM)的概念
总的说起来,无论多复杂的操作,都只是先进行虚拟DOM的JS计算,把这个组件对象计算好了以后,再一次性的通过Diff算法进行渲染或者更新,而不是每次都要直接操作真实的DOM。
在即时编译的时代,调用DOM的开销是很大的。而Virtual DOM的执行完全都在Javascript 引擎中,完全不会有这个开销。
组件的本源
知道了什么是虚拟DOM以及组件的本质后,我们还是来看一下代码吧
先从生成组件开始切入,因为要生成组件就肯定会去找组件是什么
createElement: createElementWithValidation复制代码
摘取一些核心概念出来看就好
function createElementWithValidation(type, props, children) { var element = createElement.apply(this, arguments); return element;}复制代码
可以看到,返回了一个element
,这个元素又是由createElement
方法生成的,顺着往下找
function createElement(type, config, children) { var props = {}; var key = null; var ref = null; var self = null; var source = null; return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);}复制代码
返回的是ReactElement
方法,感觉已经很近了
var ReactElement = function (type, key, ref, self, source, owner, props) { var element = { $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: ref, props: props, _owner: owner }; return element;};复制代码
bingo,返回了一个对象,再看这个对象,是不是跟上面打印出来的对象格式很像?再看一眼
这就是组件的本源
组件三种基类
前面说了,版本16.2.0中,封装了三种组件基类:分别是组件、纯组件、异步组件
Component: Component,PureComponent: PureComponent,unstable_AsyncComponent: AsyncComponent,复制代码
一个个去看一下区别在哪里,先看Component
function Component(props, context, updater) { this.props = props; this.context = context; this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue;}复制代码
很简单,一个构造函数,通过它构造的实例对象有四个私有属性,refs
则是个emptyObject
,看名字就知道是空对象 这个emptyObject
也是引入的插件
var emptyObject = require('fbjs/lib/emptyObject');复制代码
再去看PureComponent
,AsyncComponent
,定义的时候居然跟Component
是一样的
区别
区别呢?
这里就需要用到原型链方面的知识了
虽然原型和继承在日常项目和工作中用的不多
但,那是因为我们平时很大部分在面向过程编程,特别是业务代码,但想要进阶,就要去读别人的源码,去自己封装组件,这时它们就派上用场了,这就是为什么它们很重要的原因。
核心的方法,和属性,以及这三种组件直接的关系都是通过原型链的知识联系起来的,关键代码如下,我画了个简图,希望能对看文章的各位有所帮助,如果有画错的,希望能指正我
先上核心代码,一些细枝末节的代码暂时忽略
setState
和forceUpdate
这两个方法挂载在Component
(组件构造器)的原型上
Component.prototype.setState = function (partialState, callback) { ...};Component.prototype.forceUpdate = function (callback) { ...};复制代码
接下来定义一个ComponentDummy
,其实也是一个构造器,按照名字来理解就是“假组件”?,它是当做辅助用的
让ComponentDummy
的原型指向Component
的原型,这样它也能访问原型上面的共有方法和属性了,比如setState
和forceUpdate
function ComponentDummy() {}ComponentDummy.prototype = Component.prototype;复制代码
下面这句话,假组件构造器ComponentDummy
实例化出来一个对象pureComponentPrototype
,然后把这个对象的constructor属性又指向了PureComponent
,因此PureComponent
也成为了一个构造器,也就是上面的第二种组件基类
var pureComponentPrototype = PureComponent.prototype = new ComponentDummy();pureComponentPrototype.constructor = PureComponent;复制代码
AsyncComponent
基类也是一样
var asyncComponentPrototype = AsyncComponent.prototype = new ComponentDummy();asyncComponentPrototype.constructor = AsyncComponent;复制代码
但是AsyncComponent
的原型多了一个方法render
,看到了吗,妈妈呀,这就是render的出处
asyncComponentPrototype.render = function () { return this.props.children;};复制代码
所以到目前为止,可以得出一个原型图
但是,有个问题来了,render
方法挂载在AsyncComponent
的原型上,那通过Component
构造器构造出来的实例岂不是读不到render
方法,那为什么日常组件是这样写的?
其实还有两句代码,上面做了个小剧透的_assign
_assign(pureComponentPrototype, Component.prototype);复制代码
_assign(asyncComponentPrototype, Component.prototype);复制代码
每句话上面还特意有个注释,Avoid an extra prototype jump for these methods.
,避免这些方法额外的原型跳转,先不管它,先看_assign
做了什么
把Component的原型跟AsyncComponent的原型合并,
那么到这里,答案就呼之欲出了,如此一来,AsyncComponent
上面的render
方法,不就相当于挂载到Component
上面了吗?
以此类推,三种基类构造器最后都是基于同一个原型,共享所以方法,包括render
、setState
、forceUpdate
等等,最后的原型图应该就变成了这样
到这里,有个问题要思考的是:
既然最后三个基类共用同一个原型,那为什么要分开来写? 中间还通过一个假组件构造器ComponentDummy
来辅助构建两个实例
源码还没读完,这个地方我目前还没弄明白,应该是后面三个基类又分别挂载了不一样的方法,希望有大佬能提前回答一下。
后话
感谢您耐心看到这里,希望有所收获!
如果不是很忙的话,麻烦点个star⭐,举手之劳,却是对作者莫大的鼓励。
我在学习过程中喜欢做记录,分享的是自己在前端之路上的一些积累和思考,希望能跟大家一起交流与进步,更多文章请看