乐趣区

Component,PureComponent源码解析

每次都信誓旦旦的给自己立下要好好学习 react 源码的 flag,结果都是因为某个地方卡住了,或是其他原因没看多少就放弃了。这次又给自己立个 flag- 坚持看完 react 源码。为了敦促自己,特开设这样一个专栏来记录自己的学习历程,这意味着这个专栏的文章质量并不高,你可以拿来参考参考,切莫全信,我不想误人子弟,后面要是学有所成再考虑产出些好点的文章。要是发现文章中有什么不当之处,欢迎批评交流。我看的源码版本是 16.8.2。我是用在源码加注释的方法学习的,放在 github 上的 learn 分支。

为了看 react 源码,我查找了不少资料,这里推荐两个参考资料,个人觉得写得不错。

慕课网一个课的电子书,他有个源码解析的视频教程,应该不错,不过我没买。

一个知乎专栏,写得很清晰,只不过是 15.6.2 的, 在 react16 里面一些方法找不到了。

Component, PureComponent 是我们最常用的东西,我们经常继承他们来创建组件。因此,我选择从这几个最最常用的东西入手开始欣赏 React 源码。他们都位于 packages/react 目录下,入口在 index.js,index.js 里边导出的其实是 src 下的 React.js 里的东西,在 React.js 中可以看到 React 暴露的 API。在 React.js 中可以找到上面说述的 Component,PureComponent 和 ReactElement 相关线索。
Component
Component 和 PureComponent 都位于 /packages/react/src/ReactBaseClasses.js。
这两个东西都是构造函数,或者称为类。
Component 的构造函数长成如下这样:
/**
* Base class helpers for the updating state of a component.
*/

// 经常去继承他, 原来这个构造行数是这样的
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.

// 这个 new 的时候需要注意 updater 是哪里来的, 这个 updater 与 setState 应该有很大关系
this.updater = updater || ReactNoopUpdateQueue;
}
这并没有什么神奇的,他接收三个参数,挂到 this 上。具体是这三个参数是啥,我目前也是不清楚的,因为我们平时使用都是 extends 他而并没有 new 他,new 的过程应该是框架去做的,这个得到后面再做分析。后面分析时需要注意 updater,感觉这里会是一个重点,他有一个默认值,ReactNoopUpdateQueue,去看了下他的代码,他是一个对象,挂了一些方法,这里也就不展开了,我也没太细看。
Component 的原型上挂了一些方法和属性,isReactComponent 属性,setState 方法,forceUpdate 方法,代码如下:
// 通常 isXxx 都是 boolean 类型的,这里比较奇怪,后面需要关注下
Component.prototype.isReactComponent = {};

/**
* … 这里有很多说明,可以直接去看
*
* @param {object|function} partialState Next partial state or function to
* produce next partial state to be merged with current state.
* @param {?function} callback Called after state is updated.
* @final
* @protected
*/

// 原来我们平时调用的 setState 就这么几行啊,但是看他是调用的 updater 的 enqueueSetState,
// 相关实现应该在那里边了, 可以 updater 这个东西很厉害
Component.prototype.setState = function(partialState, callback) {
// 这里是个参数校验,校验不通过的话会给提示信息,并抛出异常
invariant(
typeof partialState === ‘object’ ||
typeof partialState === ‘function’ ||
partialState == null,
‘setState(…): takes an object of state variables to update or a ‘ +
‘function which returns an object of state variables.’,
);
this.updater.enqueueSetState(this, partialState, callback, ‘setState’);
};

/**
* … 这里有很多说明,可以直接去看
*
* @param {?function} callback Called after update is complete.
* @final
* @protected
*/

// 很少用到这个方法啊, 但他和 setState 一样都是 Component 原型上的方法
Component.prototype.forceUpdate = function(callback) {
this.updater.enqueueForceUpdate(this, callback, ‘forceUpdate’);
};
其实 Component 的原型上挂载的东西也没什么神奇的,其中非常重要的是 updater 的 enqueueSetState,enqueueForceUpdate 方法,进一步说明了 updater 是后面分析的重点。
接下来的一段代码是用来在开发模式下标记废弃的 api 的,在开发模式下回给写提示,代码如下:
// 这里是标识一些废弃的 api, 开发模式会报出来提醒开发这注意
if (__DEV__) {
const deprecatedAPIs = {
isMounted: [
‘isMounted’,
‘Instead, make sure to clean up subscriptions and pending requests in ‘ +
‘componentWillUnmount to prevent memory leaks.’,
],
replaceState: [
‘replaceState’,
‘Refactor your code to use setState instead (see ‘ +
‘https://github.com/facebook/react/issues/3236).’,
],
};
const defineDeprecationWarning = function(methodName, info) {
Object.defineProperty(Component.prototype, methodName, {
get: function() {
lowPriorityWarning(
false,
‘%s(…) is deprecated in plain JavaScript React classes. %s’,
info[0],
info[1],
);
return undefined;
},
});
};
for (const fnName in deprecatedAPIs) {
if (deprecatedAPIs.hasOwnProperty(fnName)) {
defineDeprecationWarning(fnName, deprecatedAPIs[fnName]);
}
}
}
__DEV__这个东西我没找到是在哪里挂到全局的(知道的同学可以留言指点下),但是看变量名可以推测他是开发模式标识,这个提示我们在做一些给别人用的东西时,接口协议约定十分重要,一旦约定就不能轻易变更,确实需要变更时需要通知调用方调整。回头来,这里标识废弃了 isMounted,replaceState 两个方法,其实他们被挪到了 updater 里边。
PureComponent
开始用 React 时老大 Rewview 我的代码时经常写评论,“你这个 Component 可以改成 PureComponent”, 当时一直不懂 PureComponent 与 Component 的区别(现在也没全懂),只是听人说 PureComponent 更新的时候是浅比较,而 Component 是深比较。今天看了这部分,其实也没懂,不过感觉后面再看看应该就懂了。要搞清这里的 PureComponet 需要了解下 js 中继承的实现,大家可以参考《JavaScript 高级程序设计》相关介绍,也可以看看理解 js 继承的 6 种方式, 笔者看到这个 PureComponet 也是先复习了下才看的。不管你看没看,代码先贴出来:
// PureComponent

function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

// 发现 PureComponnet 的构造方法和 Component 是相同的
/**
* Convenience component with default shallow equality check for sCU.
*/
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.

// 感觉不用加也可以, 只不过会多查找一次,但是不得不说细节考虑的真棒

Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
我画了个图来理解这个继承。

首先是创建了一个 ComponentDummy 构造函数,他的原型指到 Component 的原型;然后创建了一个 PureComponent, 加上了和 Component 一样的属性(这里为啥不用 call)。PureComponent 的原型指向 ComponentDummy 的实例;修改 PureComponent 原型的 constructor 属性使其正确指向 PureComponent 的构造函数,并挂一个 isPureReactComponent 的属性。为了减少向上去查找原型链次数,用了一个 assign 直接将 Component 原型的东西拷贝到 PureComponent 的原型上(这里还是考虑的比较精细的)。
首先这个实现没有啥问题,但是我有个疑问,大家可以留言指点下:
为什么要用继承,注意到 PureComponent 的构造函数和 Component 是一样的,然后还有一个拷贝 Component 的原型到 PureComponent 的原型的操作,那这里有继承的必要吗?不都是重写的吗,感觉多此一举。
下一篇预告 ReactElement 源码解析

退出移动版