Regular源码阅读之extend

阿凡达2018-06-28 09:48

Regular.extend

前言

其实在论坛上已经有了一篇讲解extend的博客,并且讲得很好,不过由于阅读代码写出博客可以加深自己的理解,所以还是自己写了一篇。

Regular组件的定义

定义一个Regular组件的代码通常长这样:

import Regular from 'regularjs';

const App = Regular.extend({
    name: 'app',

    data: {
        someprop: 123
    },

    config(data) {}
    ....
})

寻找extend函数所在

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(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构造函数会执行两次:

  • B.prototype = new A()//第一次
  • A.apply(this, arguments); //第二次 这个会造成A的实例属性不仅会在B的实例上有,在B的原型上也会有。 所以更聪明的人就想到了借助第三方构造函数:

function F() {}

F.prototype = A.prototype;

function B() {
   A.apply(this, arguments);
}

B.prototype = new F();
B.prototype.constructor = A;

继承满足了我们的所有要求,现在我们接着看Regular.extend做的事情

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());
}

定义了一个构造函数fn

function fn() {
    supr.apply(this, arguments);
}

上面代码中supr为Regular(或者父级Regular组件构造函数),构造函数中使用apply继承了父类的实例属性和方法

createProto

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
}

implement

 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

process

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()来执行父类同属性方法。

wrap

/**
 * @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()就不对了。

本文来自网易实践者社区,经作者何美玲授权发布。