Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
amandakelake opened this issue Mar 4, 2018 · 15 comments
Open

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

amandakelake opened this issue Mar 4, 2018 · 15 comments

Comments

@amandakelake
Copy link
Owner

amandakelake commented Mar 4, 2018

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

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

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

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

mkdir [email protected]
cd [email protected]
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来辅助构建两个实例

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

@Can2Nya
Copy link

Can2Nya commented Mar 18, 2018

支持一下!

@hicoldcat
Copy link

期待更新

@iweijie
Copy link

iweijie commented Aug 24, 2018

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

@amandakelake
Copy link
Owner Author

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

@edxxu
Copy link

edxxu commented Oct 23, 2018

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

@CodeVam
Copy link

CodeVam commented Dec 13, 2018

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

@pywmm
Copy link

pywmm commented Jan 18, 2019

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

@LJJCherry
Copy link

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

@changecoder
Copy link

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

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

@Alisa0026
Copy link

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

@yangrenmu
Copy link

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

@lanyaosmy
Copy link

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

@Liuyll
Copy link

Liuyll commented Dec 14, 2019

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

@wingtao
Copy link

wingtao commented Dec 23, 2019

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

@pablezhang
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests