其实在论坛上已经有了一篇讲解extend的博客,并且讲得很好,不过由于阅读代码写出博客可以加深自己的理解,所以还是自己写了一篇。
定义一个Regular组件的代码通常长这样:
import Regular from 'regularjs';
const App = Regular.extend({
name: 'app',
data: {
someprop: 123
},
config(data) {}
....
})
regularjs的入口文件中:/lib/index.js,在这个文件我们可以看到:
var Regular = module.exports = require("./render/client");
Regular构造函数在lib/render/client.js里面定义,有代码如下:
...
var extend = require('../helper/extend');
...
var Regular = function(definition, options){
...
}
...
extend(Regular);
...
我们转到lib/helper/extend.js,这里就是extend的代码所在。
上面的代码是extend(Regular)而不是Regular.extend的形态,但是我们看到extend函数的开头是这样的:
module.exports = function extend(o){
o = o || {};
var supr = this, proto,
supro = supr && supr.prototype || {};
if(typeof o === 'function'){
proto = o.prototype;
o.implement = implement;
o.extend = extend;
return o;
}
...
}
很显然,如果发现传入的参数是个函数,那么会设置o.extend = extend然后就结束,这时候Regular.extend === extend。
我们先停下来说下通俗易懂的原型继承的写法(《高程》有很详细的讲解,这里献丑了)
function A() {
this.props = 'value'
}
A.prototype = {
constructor: A,
someMethod: function() {
console.log('hello')
}
}
//想定义一个类B,继承A,要做两件事:(1)继承实例属性(2)继承原型方法
//原型继承就是让B的prototype等于一个A的实例,这样就形成了原型链
function B() {}
B.prototype = new A()
var b = new B();
b.props; // 'value'
b.sayHello() //'hello'
但是以上代码有个很明显的缺陷,那就是b本身是没有props这个属性的,props在b的原型上,所以:
b.hasOwnProperty('props') //false
b.__proto__.hasOwnProperty('props') // true
为了解决实例属性的问题,我们应该在B的构造函数里执行:
function B() {
this.props = 'value';
}
上面的代码明显不可取,因为实例属性不可能总是确定,这个方法不通用,聪明的人想到了通过apply来执行A的函数,于是B应该写成:
function B() {
A.apply(this, arguments);
}
这样看来已经很完美了,我们不仅继承了A的实例属性,还继承了A的原型方法,不过代码就是这样迷人,因为它总是可以变得更完美。 不知道你是否注意到上面的继承方法中,如果执行new B()的时候,A构造函数会执行两次:
function F() {}
F.prototype = A.prototype;
function B() {
A.apply(this, arguments);
}
B.prototype = new F();
B.prototype.constructor = A;
继承满足了我们的所有要求,现在我们接着看Regular.extend做的事情
下面开始Regular.extend的执行过程:
var supr = this, proto, //由于是执行Regular.extend,所以this指向Regular,如果是xxx.extend,this就指向xxx(Rgular组件)
supro = supr && supr.prototype || {};
function fn() {
supr.apply(this, arguments);
}
proto = _.createProto(fn, supro);
createProto为:
_.createProto = function(fn, o){
function Foo() { this.constructor = fn;}
Foo.prototype = o;
return (fn.prototype = new Foo());
}
function fn() {
supr.apply(this, arguments);
}
上面代码中supr为Regular(或者父级Regular组件构造函数),构造函数中使用apply继承了父类的实例属性和方法
function Foo() {
this.constructor = fn
}
Foo.prototype = o //o === supro === supr.prototype
//相当于:fn.prototype = new Foo();
// return fn.prototype
return (fn.prototype = new Foo());
是不是和上面说的一模一样,差别在于:
B.prototype.constructor = A;
它在Foo的构造函数里面做了:
function Foo() {
this.constructor = fn
}
function implement(o){
// we need merge the merged property
var len = mlen;
for(;len--;){
var prop = merged[len];
if(proto[prop] && o.hasOwnProperty(prop) && proto.hasOwnProperty(prop)){
_.extend(proto[prop], o[prop], true)
delete o[prop];
}
}
process(proto, o, supro);
return this;
}
fn.implement = implement
fn.implement(o)
代码中执行fn.implement(o)//o为我们extend传进来的对象参数,首先看到:
var len = mlen;
for(;len--;){
var prop = merged[len]; //merged === ['data', 'computed']
if(proto[prop] && o.hasOwnProperty(prop) && proto.hasOwnProperty(prop)){
_.extend(proto[prop], o[prop], true)
delete o[prop];
}
}
这段话是什么意思呢?其实很简单,(1)从_.extend我们可以看出是在做配置项data和computed的合并(2)接着我们可以分析条件语句得到只有当proto有data和computed(不能是proto的prototype有),Regular.extend里面proto到这里是不会有data和computed的(在process方法执行后才会有)所以extend的时候,这里总是不执行的。
所以我们判断出这个是为了在组件implement的时候执行的,例如:
const App = Regular.extend({
data: {
}
}).implement({
data: {
}
})
这时候就会去执行以上代码的合并,_.extend的第三个参数为true,相当于assign
var hooks = {
events: function( propertyValue, proto ){
var eventListeners = proto._eventListeners || [];
var normedEvents = _.normListener(propertyValue);
if(normedEvents.length) {
proto._eventListeners = eventListeners.concat( normedEvents );
}
delete proto.events ;
}
}
//执行的是process(proto, o, supro);
function process( what, o, supro ) {
for ( var k in o ) {
if (o.hasOwnProperty(k)) {
if(hooks.hasOwnProperty(k)) {
hooks[k](o[k], what, supro)
}
what[k] = isFn( o[k] ) && isFn( supro[k] ) &&
fnTest.test( o[k] ) ? wrap(k, o[k], supro) : o[k];
}
}
}
isFn = function(o){return typeof o === "function"}
fnTest = /xy/.test(function(){"xy";}) ? /\bsupr\b/:/.*/
估计对于fnTest你可能有疑问,我的理解是在执行test的时候,由于test接受string作为参数,所以如果你传入一个函数,那么这个函数会被转化,执行toString(),这个函数是想测试function.toString()的行为正不正常(我的理解,如果有大神有别的看法,麻烦留言告知,感激不尽)。 所以process里面会对属性值做一个判断,如果为函数,并且父类这个属性也是一个函数,并且子类函数里面有supr(),那么会wrap来包装这个函数,使得在子类中可以通过this.supr()来执行父类同属性方法。
/**
* @param {String} k 属性key
* @param {Function} fn 子类方法
* @param {Object} supro 父类原型
*/
function wrap( k, fn, supro ) {
return function () {
var tmp = this.supr;
this.supr = supro[k];
var ret = fn.apply(this, arguments);
this.supr = tmp;
return ret;
}
}
wrap是一个高阶函数,返回一个函数。返回的这个函数中首先纪录this.supr,然后将父类的方法挂在到this.supr上,然后执行子类方法,最后还原this.supr。代码很少,主要讲讲为什么要纪录this.supr,原因是当存在this.supr的嵌套的时候,这时候就需要纪录,如下面代码:
const App = Regular.extend({
fn1() {
},
fn2() {
}
})
const Sub = App.extend({
fn1() {
this.supr()
},
fn2() {
this.supr();
this.fn1();
this.supr();
}
})
new Sub().fn2()
代码fn2中,执行了fn1如果这时候不还原supr,supr就变成了App的fn1,那么接下来的第二个this.supr()就不对了。
本文来自网易实践者社区,经作者何美玲授权发布。