amandakelake/blog

基于React版本16的源码解析(一):组件实现

amandakelake opened this issue · 15 comments

本次分析的源码采用的是16.2.0的版本
目前网上现有的react源码分析文章基于的都是版本16以前的源码,入口和核心构造器不一样了,如下图所示
47661520138666_ pic_hd

本想借鉴前人的源码分析成果,奈何完全对不上号,只好自己慢慢摸索

水平有限,如果有错误和疏忽的地方,还请指正。

最快捷开始分析源码的办法

mkdir analyze-react@16.0.2
cd analyze-react@16.0.2
npm init -y
npm i react --save

然后打开项目,进入node_nodules => react
先看入口文件index.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');
}

我们就看开发环境下的版本吧,压缩版本是打包到生产环境用的

打开图中文件即可
9bdbeb27-6d44-4ada-9b01-8fd009ba66dc

核心接口

分析源码先找对外的暴露接口,当然就是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,

  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
    ReactCurrentOwner: ReactCurrentOwner,
    // Used by renderers to avoid bundling object-assign twice in UMD bundles:
    assign: _assign
  }
};

ReactChildren

ReactChildren提供了处理 this.props.children 的工具集,跟旧版本的一样

Children: {
    map: mapChildren,
    forEach: forEachChildren,
    count: countChildren,
    toArray: toArray,
    only: onlyChild
  },

组件

旧版本只有ReactComponent一种
新版本定义了三种不同类型的组件基类ComponentPureComponent 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看一下是什么
0a82218a-5d5b-4df4-b6bf-b6260a0c3959

npm start启动项目看看
fcc8abac-5b68-4455-8d9b-49687ea5c526

其实就是个对象,有很多属性,注意到props里面, 没有内容
给组件A里面包含一点内容

componentDidMount() {
    console.log('组件A',<A><span>加点内容看看</span></A>)
  }

3d1cec07-fb01-4338-9edc-6dd579dcf333

可以看到,props.children里面开始嵌套内容了
那以我们聪明的程序员的逻辑来推理一下,其实不断的页面嵌套,就是不断的给这个对象嵌套props而已
不信再看一下

componentDidMount() {
    console.log('组件A',<A><span>加点内容看看<a>不信再加多一点</a></span></A>)
  }

d55f6b7a-4731-46d9-853b-6716d854c0bf

虚拟DOM概念

所以到目前为止,我们知道了react的组件只是对象,而我们都知道真正的页面是由一个一个的DOM节点组成的,在比较原生的jQuery年代,通过JS来操纵DOM元素,而且都是真实的DOM元素,而且我们都知道复杂或频繁的DOM操作通常是性能瓶颈产生的原因
所以React引入了虚拟DOM(Virtual DOM)的概念
React虚拟DOM浅析 | AlloyTeam
总的说起来,无论多复杂的操作,都只是先进行虚拟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,返回了一个对象,再看这个对象,是不是跟上面打印出来的对象格式很像?再看一眼
60b10871-5e00-4c59-a3bc-0d4760931b10

这就是组件的本源

组件三种基类

前面说了,版本16以后,封装了三种组件基类:分别是组件、纯组件、异步组件

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');

再去看PureComponentAsyncComponent,定义的时候居然跟Component 是一样的
12447d83-650e-4f87-b49b-bedf54270e64
d579b658-177f-4db0-bf3f-28505d2ea17b

都是这四句话

this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;

区别呢?这里就要比较理解原型链方面的知识了
虽然原型和继承在日常项目和工作中用的不多,那是因为我们在面向过程编程,但想要进阶,就要去读别人的源码,去自己封装组件,这事它们就派上用场了,这就是为什么它们很重要的原因。

核心的方法,和属性,以及这三种组件直接的关系都是通过原型的知识联系起来的,关键代码如下,我画了个简图,希望能对看文章的各位有所帮助,如果有画错的,希望能指正我

先上核心代码
setStateforceUpdate这两个方法挂载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;
};

所以到目前为止,可以得出一个原型图
d982ca9a-57d2-4e6b-9833-25bc60ef08db

但是,有个问题来了,render方法挂载在AsyncComponent的原型上,那通过Component构造器构造出来的实例岂不是读不到render方法,那为什么日常组件是这样写的?
84b77775-7e86-4ca6-8e16-51bbe85ef851

还有两句代码,上面做了个小剧透的_assign

// Avoid an extra prototype jump for these methods.
_assign(pureComponentPrototype, Component.prototype);
// Avoid an extra prototype jump for these methods.
_assign(asyncComponentPrototype, Component.prototype);

每句话上面还特意有个注释,Avoid an extra prototype jump for these methods.,避免这些方法额外的原型跳转,先不管它,先看_assign做了什么,
把Component的原型跟AsyncComponent的原型合并
那么这里答案就呼之欲出了,如此一来,AsyncComponent上面的render方法,不就相当于挂载到Component上面了吗?

以此类推,三种基类构造器最后都是基于同一个原型,共享所以方法,包括render、setState、forceUpdate等等,最后的原型图应该就变成了这样

c18957ce-adf9-4f5c-8d64-ac2309d73bfb

到这里,有个问题要思考的是?
既然最后三个基类共用同一个原型,那为什么要分开来写?
中间还通过一个假组件构造器ComponentDummy来辅助构建两个实例

源码还没读完,这个地方我目前还没弄明白,应该是后面三个基类又分别挂载了不一样的方法,希望有大佬能提前回答一下

支持一下!

期待更新

求画图工具,以及期待更新

@weijie9520 画图工具是Axure
最近在写vue 说来惭愧,好久没用react了,得抓紧补上

edxxu commented

请问用的是 Axure 的什么元件库画的图?

@amandakelake 会有不同的属性分别挂在上面isReactComponent和isPureReactComponent,用来判断是纯组件还是有状态组件和无状态组件,isPureReactComponent的时候会执行state和props的浅层判断来调用shouldUpdate

pywmm commented

中间那段ComponentDummy应该是典型的寄生组合式继承吧,楼主啥时候更新期待ing

16.3的版本之后没有了AsyncComponent,那render方法是哪来的呢?

16.3的版本之后没有了AsyncComponent,那render方法是哪来的呢?

最新版本master上 render方法是调用的ReactRoot对象属性方法 后面的绑定和更新操作都是基于FiberNode 这个对象

写的蛮好的,说明了版本,跟着源码看,挺清晰的,期待后面的更新

_assign(pureComponentPrototype, Component.prototype),是将Component.prototype对象复制到pureComponentPrototype对象上,Component.prototype对象并没改变吧?

Component的原型上面没有render方法...
{isReactComponent: {…}, setState: ƒ, forceUpdate: ƒ, constructor: ƒ, …} forceUpdate: ƒ (callback) isReactComponent: {} setState: ƒ (partialState, callback) constructor: ƒ Component(props, context, updater) isMounted: (...) replaceState: (...)

pureComponent和AsyncComponent有共同的东西,这个共同的东西不能污染到Component,所以添加了中间继承类ComponentDummy,_assign的解析有问题,后面的React已经修改为Object.assign,所以Component上并没有融合的render函数

ComponentDummy.prototype的constructor还是Component吧,并不能指向ComponentDummy或者PureComponent

大家读源码都从node_modules开始的? 我把react的源码clone下来发现,项目文件结构易于常规项目