天天看點

深入解讀ember.js的對象體系

write by yinmingjun,引用請注明。

如果需要了解ember.js架構,最好的方式是閱讀它的代碼,作為閱讀代碼的基礎,需要了解ember.js代碼的組織形式。ember.js采用的原型法的繼承體系,并且在内部維護着類型體系的中繼資料,并且通過引入觀察者的模式來關注各個對象資料的變更,對屬性的維護也提供了get/set基礎服務。ember的對象體系滲透在ember.js架構的各個環節,組織形式相對複雜,我們作為解讀ember.js的開篇,從分析ember.js的對象體系開始入手。

一、class和instance

1、class的定義

在ember.js中,定義一個class是通過父類的extend類方法(請允許我這麼表達,是指那些寄存在類的類型上的方法,通過類的類型來引用和調用的方法,下同)來定義的。如果沒有父類,可以從Ember.Object開始。

我們看一個例子:

    App.Person = Ember.Object.extend({

        say: function(thing) {

            alert(thing);

        }

    });

上面這個例子建立了一個App.Person的類,從Ember.Object派生而來。基于這種方式,我們可以從任何一個類派生出新的class。

在派生的類中可以override父類的方法,如果需要通路父類的實作,可以使用特殊的this._supper()方法,調用父類的實作。

例如:

    App.Person = Ember.Object.extend({

        say: function(thing) {

             var name = this.get('name');

             alert(name + " says: " + thing);

        }

    });

    App.Soldier = App.Person.extend({

          say: function(thing) {

                this._super(thing + ", sir!");

          }

    });

2、建立instance

定義類之後,可以建立類的執行個體,在ember.js中,建立類的執行個體是通過調用類的create類方法來建立類的執行個體。

例如:

    var person = App.Person.create();

    person.say("Hello") // alerts "Hello"

在使用create方法建立類的執行個體的時候,可以傳遞一個object(就是{},一個散列對象表)作為參數,作為執行個體的屬性的初值,建議隻傳遞簡單的對象,改寫執行個體的方法還是通過派生類的方式來做。

例如:

    Person = Ember.Object.extend({

        helloWorld: function() {

            alert("Hi, my name is " + this.get('name'));

        }

    });

    var tom = Person.create({

        name: "Tom Dale"

    });

    tom.helloWorld() // alerts "Hi my name is Tom Dale"

3、類執行個體的初始化

在類的執行個體建立之後,它的init方法(如果存在)會被自動的調用,這是一個初始化類執行個體的理想的場所。

例如:

    Person = Ember.Object.extend({

        init: function() {

            var name = this.get('name');

            alert(name + ", reporting for duty!");

        }

    });

    Person.create({

        name: "Stefan Penner"

    });

    // alerts "Stefan Penner, reporting for duty!"

使用init方法初始化類的時候,需要注意不要忘記調用父類的init方法(通過this._supper()方法),一些類的實作依賴其init方法,如Ember.View或Ember.ArrayController。

4、類和執行個體的reopen的概念

在ember.js中,運作類不是一次的定義完畢,運作在其它的地方追加類的定義(動态語言特有的優勢),ember.js稱呼這種方式為reopen。

我們還是通過例子來看類的reopen。

例子:

    Person.reopen({

        isPerson: true

    });

    Person.create().get('isPerson') // true

通過class的reopen類方法,向類追加isPerson屬性,并給出true的初值,這樣,建立的類的執行個體馬上就可以通路到該屬性。

同樣,在reopen類方法中,可以override類的方法,并使用this._supper()方法來通路前一個版本的實作。

例如:

    PersonBase = Ember.Object.extend({

        say: function(thing) {

            alert(thing);

        }

    });

    Person = PersonBase.extend({

        say: function(thing) {

            this._super(">>>"+thing);

        }

    });

    Person.reopen({

         // override `say` to add an ! at the end

         say: function(thing) {

             this._super(thing + "!");

         }

    });

    Person.create().say('hello');

    //alert message : '>>>hello!'

reopen中,this._super()是前一個版本的實作,而不是父類的實作,這對改寫類的實作很有幫助。

與類的reopen類方法類似的還有reopenClass類方法,這個方法是擴充類型上的方法和屬性。

例如:

    Person.reopenClass({

        createMan: function() {

           return Person.create({isMan: true})

        }

    });

    Person.createMan().get('isMan') // true

除了方法,也可以向類型上添加屬性,直接通過類型拉通路。

二、類的屬性

1、類屬性的通路

如果需要通路類的屬性,需要通過this.get()和this.set()通路器來操作,直接修改對象的資料成員會繞過ember.js的觀察者模式。

例如:

    var person = App.Person.create();

    var name = person.get('name');

    person.set('name', "Tobias Fünke");

2、類的計算屬性(Computed Properties)

有些情況下,類的屬性值是來自類的其他屬性值的計算結果,這種屬性成為計算屬性,ember.js對計算屬性提供了支援。

例如:

    Person = Ember.Object.extend({

        // these will be supplied by `create`

        firstName: null,

        lastName: null,

        fullName: function() {

            var firstName = this.get('firstName');

            var lastName = this.get('lastName');

            return firstName + ' ' + lastName;

        }.property('firstName', 'lastName')

    });

    var tom = Person.create({

         firstName: "Tom",

         lastName: "Dale"

    });

    tom.get('fullName') // "Tom Dale"

計算屬性是通過function的property方法來定義的,其傳回值就是屬性的通路結果。

還有些情況下,還會希望計算屬性能設定屬性值,可以

    Person = Ember.Object.extend({

        // these will be supplied by `create`

        firstName: null,

        lastName: null,

        fullName: function(key, value) {

             // getter

             if (arguments.length === 1) {

                 var firstName = this.get('firstName');

                 var lastName = this.get('lastName');

                 return firstName + ' ' + lastName;

            // setter

            } else {

                var name = value.split(" ");

                this.set('firstName', name[0]);

                this.set('lastName', name[1]);

                return value;

            }

       }.property('firstName', 'lastName')

   });

   var person = Person.create();

   person.set('fullName', "Peter Wagenet");

   person.get('firstName') // Peter

   person.get('lastName') // Wagenet

也就是說,對計算屬性的set會傳遞key和value作為參數;get僅會傳遞key作為參數。

注釋1:

由于計算屬性可能帶來的計算開銷,ember.js會緩存計算結果,加速對計算屬性的通路。emer.js通過依賴項的變更通知來維護計算屬性的緩存,是以需要通過function的property方法來登記依賴的其他屬性。

注釋2:

估計很多人會希望了解ember.js的計算屬性的魔法的細節,稍稍透漏一點,大家可以按圖索骥。

function的property方法在ember.js中的代碼如下:

    Function.prototype.property = function() {

        var ret = Ember.computed(this);

        return ret.property.apply(ret, arguments);

    };

3、類的聚集屬性(Aggregate Properties)

嚴格意義上來說,聚集屬性是計算屬性的一種,不過大多使用者還是願意将聚集屬性單獨做一個分類。 

    App.todosController = Ember.Object.create({

        todos: [

            Ember.Object.create({ isDone: false })

        ],

        remaining: function() {

             var todos = this.get('todos');

             return todos.filterProperty('isDone', false).get('length');

        }.property('[email protected]')

    });

聚集屬性的魔法是通過@each來描述依賴的一個屬性的數組,擷取數組元素和數組本身的變更、增減,并保持計算結果緩存的有效性。

三、觀察者(Observers)模式

在ember.js的任何對象(從Ember.Object派生而來),都可以通過其addObserver方法和addBeforeObserver來添加資料變更的觀察者。ember.js類的屬性,包括計算屬性,都可以添加觀察者。

例如:

    Person = Ember.Object.extend({

        // these will be supplied by `create`

        firstName: null,

        lastName: null,

        fullName: function() {

            var firstName = this.get('firstName');

            var lastName = this.get('lastName');

            return firstName + ' ' + lastName;

        }.property('firstName', 'lastName')

    });

    var person = Person.create({

         firstName: "Yehuda",

         lastName: "Katz"

    });

    person.addObserver('fullName', function() {

         // deal with the change

    });

    person.set('firstName', "Brohuda"); // observer will fire

上面的代碼對person執行個體的fullName屬性,添加了一個觀察change的觀察者。

因為觀察者在ember.js中非常常見,ember提供了針對class的觀察者的書寫方式:

    Person.reopen({

        fullNameChanged: function() {

            // this is an inline version of .addObserver

        }.observes('fullName')

    });

或者,可以借助Ember.observer方法,将上面的代碼改寫為:

    Person.reopen({

        fullNameChanged: Ember.observer(function() {

            // this is an inline version of .addObserver

        }, 'fullName')

    });

Ember.addBeforeObserver和Ember.addObserver類似,不同之處是添加變更的前事件。

四、綁定(Bindings)的支援

在ember.js中,将綁定看成兩個屬性之間的關聯,通過觀察者來保持資料的一緻性。

在ember中,綁定分為雙向綁定和單向綁定,我們分别介紹一下。

1、雙向綁定(Two-Way Bindings)的支援

最簡單的建立雙向資料綁定的方式,是建立一個以Binding結尾的屬性(在ember中的形式化的文法很常見,有趣),并指定需要綁定的屬性的fullName。

例如:

    App.wife= Ember.Object.create({

        householdIncome: 80000

    });

    App.husband= Ember.Object.create({

        householdIncomeBinding: 'App.wife.householdIncome'

    });

    App.husband.get('householdIncome'); // 80000

    // Someone gets raise.

    App.husband.set('householdIncome', 90000);

    App.wife.get('householdIncome'); // 90000

上面的代碼在App.husband中建立了一個名字為householdIncome的屬性(什麼名字都可以,以Binding結尾就可以),并建立到App.wife.householdIncome的雙向的綁定。這樣,在App.husband中操作資料和App.wife中操作資料是一樣的。

2、單向綁定(One-Way Bindings)的支援

在ember中,還存在一種綁定模式,變更隻是從綁定源頭向建立綁定的屬性同步變更,而不會将綁定屬性的變更同步回綁定的源頭,這種方式稱為單向綁定。

在ember中使用單向綁定大多是基于性能的原因,如果沒什麼問題還是應該使用雙向的綁定。

例子:

    App.user = Ember.Object.create({

         fullName: "Kara Gates"

    });

    App.userView = Ember.View.create({

         userNameBinding: Ember.Binding.oneWay('App.user.fullName')

    });

    // Changing the name of the user object changes

    // the value on the view.

    App.user.set('fullName', "Krang Gates");

    // App.userView.userName will become "Krang Gates"

    // ...but changes to the view don't make it back to

    // the object.

    App.userView.set('userName', "Truckasaurus Gates");

    App.user.get('fullName'); // "Krang Gates"

五、一些技術實作的内幕

在前面的介紹中,大家基本上了解了ember.js對象體系的規劃,也許會對ember提供的衆多特性會感到很興奮與好奇,想隻知道how。接下來在這個章節中,我們會深入解讀ember.js源代碼,解析ember.js一些關鍵特性的實作方式,欣賞ember.js在技術領域對javascript社群的貢獻。

下面,我們按層次遞進,來看看ember.js的内在美。

1、Mixin的概念

先說一下ember.js的Mixin的概念:

Mixin,表示混合,有點類似AOP設計模式,或者C++中的多重繼承(Multiple Inheritance)。mixin本身隻提供實作,而在類的基礎中可以在基礎的類之後添加mixin,表示引用mixin中的實作,就像類本身定義的方法一樣。

在ember.js中,mixin通過Ember.Mixin.create建立,并支援mixin的繼承,最終,在類的定義的時刻将mixin中的方法按次序合并到類中。

我們看看mixin的建立、繼承與多重繼承:

    Ember.Enumerable = Ember.Mixin.create({

        //code ……

    }) ;

    Ember.Array = Ember.Mixin.create(

       Ember.Enumerable, 

       {

           //code …….

       }

    ) ;

    var NativeArray = Ember.Mixin.create(Ember.MutableArray, Ember.Observable, Ember.Copyable, {

        //code ……

    });

ember.js的mixin的方式可以很友善的複用代碼,值得推薦。

2、Mixin的資料結構

我們看看ember.js的Ember.Mixin是怎麼定義的:

Ember.Mixin= function() { return initMixin(this, arguments); };

也就是說,如果執行new Ember.Mixin(),就會轉到initMixin方法。

為了統一行為模式,Ember.Mixin也提供了create類型方法:

Mixin.create = function() {

  Ember.anyUnprocessedMixins = true;

  var M = this;

  return initMixin(new M(), arguments);

};

結合前面的代碼,可以看明白,Mixin的create方法建立了一個Mixin(無參數),并調用initMixin将參數合并到mixin之中。

看看initMixin的代碼可以發現,如果存在參數清單,會将參數放到目前mixin的mixins資料成員之中: 

function initMixin(mixin, args) {

  if (args && args.length > 0) {

    mixin.mixins = a_map.call(args, function(x) {

      if (x instanceofMixin) { return x; }

      // Note: Manually setup a primitive mixin here. This is the only

      // way to actually get a primitive mixin. This way normal creation

      // of mixins will give you combined mixins...

      var mixin= new Mixin();

      mixin.properties = x;

      return mixin;

    });

  }

  return mixin;

}

注意标記出來的initMixin對非mixin類型的字典的處理方式。

這樣,所有的包含的mixin的對象會形成一個樹形的資料結構,關于mixin的組織結構,我們先寫到這,後面的mixins的合并,我們在對象建立的過程中來講解。

3、class Type的建構過程(較長)

這部分我們來看ember.js的繼承體系的實作方式,了解ember.js如何維護父子關系,以及如何建立出來第一個基礎基類。

在ember.js中,所有類的基類是Ember.Object,在代碼中的定義如下:

Ember.Object= Ember.CoreObject.extend(Ember.Observable);

可以注意兩點:

(1) 處于簡化Object的代碼、提高源代碼整體的可讀性的目的(純粹是筆者的猜測),ember.js提供了Ember.CoreObject來封裝Object的大多功能;

(2) 在Object的級别上,ember.js就将Ember.ObservableMixin合并進來,也就是說,對于一切ember.js對象,都是可觀察的;

Ember.Object的大多API都寄存在Ember.CoreObject之上,我們繼續追蹤Ember.CoreObject的實作。

>>>>>>>>>>>>>Trace CoreObject

Ember.CoreObject的定義:

var CoreObject= makeCtor();

CoreObject.PrototypeMixin = Mixin.create({

  reopen: function() {

    applyMixin(this, arguments, true);

    return this;

  },

  isInstance: true,

  init: function() {},

  concatenatedProperties: null,

  isDestroyed: false,

  isDestroying: false,

  destroy: function() {

    if (this.isDestroying) { return; }

    this.isDestroying = true;

    schedule('actions', this, this.willDestroy);

    schedule('destroy', this, this._scheduledDestroy);

    return this;

  },

  willDestroy: Ember.K,

  _scheduledDestroy: function() {

    if (this.isDestroyed) { return; }

    destroy(this);

    this.isDestroyed = true;

  },

  bind: function(to, from) {

    if (!(from instanceof Ember.Binding)) { from = Ember.Binding.from(from); }

    from.to(to).connect(this);

    return from;

  },

  toString: function toString() {

    var hasToStringExtension = typeof this.toStringExtension === 'function',

        extension = hasToStringExtension ? ":" + this.toStringExtension() : '';

    var ret = '<'+this.constructor.toString()+':'+guidFor(this)+extension+'>';

    this.toString = makeToString(ret);

    return ret;

  }

});

CoreObject.PrototypeMixin.ownerConstructor = CoreObject;

CoreObject.__super__= null;

原來的代碼比較長,我修整了一下,隻提供幹貨,去掉了注釋,便于閱讀代碼。

CoreObject.PrototypeMixin的作用是提供instance級别的prototype,我們後面會講,現在先跳過去,知道有這麼一個東東就好了。

接下來是makeCtor方法,這個方法中建立了CoreObject類型。為了更好的閱讀代碼,我講makeCtor的代碼分成了兩個部分。

makeCtor方法代碼Part I:

function makeCtor() {

  var wasApplied = false, initMixins, initProperties;

  var Class= function() {

     //code ......, see part II

  };

  Class.toString= Mixin.prototype.toString;

  Class.willReopen = function() {

    if (wasApplied) {

      Class.PrototypeMixin = Mixin.create(Class.PrototypeMixin);

    }

    wasApplied = false;

  };

  Class._initMixins= function(args) { initMixins = args; };

  Class._initProperties= function(args) { initProperties = args; };

  Class.proto= function() {

    var superclass= Class.superclass;

    if (superclass) { superclass.proto();}

    if (!wasApplied) {

      wasApplied = true;

      Class.PrototypeMixin.applyPartial(Class.prototype);

      rewatch(Class.prototype);

    }

    return this.prototype;

  };

  return Class;

}

makeCtor方法的目标是從無到有建立出一個class類型,并提供了class的預設構造實作(會在new Class()的時候被調用),為了友善閱讀,這部分代碼放在makeCtor方法代碼Part II中給出。

這部分代碼很明确:

> 提供class類型

> 提供class級别的_initMixins和_iniProperties方法,用于設定需要合并的mixin和初始化的參數清單

> 用于重建class.prototype的proto方法

> 提供willReopen類方法,會将目前的ProtoTypeMixin打包到一個mixin之中,很不起眼的一段代碼,卻是reopen之後可以通路this._supper()方法的關鍵所在。

makeCtor方法代碼Part II:

var Class= function() {

    if (!wasApplied) {

      Class.proto();// prepare prototype...

    }

    o_defineProperty(this, GUID_KEY, undefinedDescriptor);

    o_defineProperty(this, '_super', undefinedDescriptor);

    var m = meta(this);

    m.proto = this;

    if (initMixins) {

      // capture locally so we can clear the closed over variable

      var mixins = initMixins;

      initMixins = null;

      this.reopen.apply(this, mixins);

    }

    if (initProperties) {

      // capture locally so we can clear the closed over variable

      var props = initProperties;

      initProperties = null;

      var concatenatedProperties = this.concatenatedProperties;

      for (var i = 0, l = props.length; i < l; i++) {

        var properties = props[i];

        Ember.assert("Ember.Object.create no longer supports mixing in other definitions, use createWithMixins instead.", !(properties instanceof Ember.Mixin));

        for (var keyName in properties) {

          if (!properties.hasOwnProperty(keyName)) { continue; }

          var value = properties[keyName],

              IS_BINDING = Ember.IS_BINDING;

          if (IS_BINDING.test(keyName)) {

            var bindings = m.bindings;

            if (!bindings) {

              bindings = m.bindings = {};

            } else if (!m.hasOwnProperty('bindings')) {

              bindings = m.bindings = o_create(m.bindings);

            }

            bindings[keyName] = value;

          }

          var desc = m.descs[keyName];

          Ember.assert("Ember.Object.create no longer supports defining computed properties.", !(value instanceof Ember.ComputedProperty));

          Ember.assert("Ember.Object.create no longer supports defining methods that call _super.", !(typeof value === 'function' && value.toString().indexOf('._super') !== -1));

          if (concatenatedProperties && indexOf(concatenatedProperties, keyName) >= 0) {

            var baseValue = this[keyName];

            if (baseValue) {

              if ('function' === typeof baseValue.concat) {

                value = baseValue.concat(value);

              } else {

                value = Ember.makeArray(baseValue).concat(value);

              }

            } else {

              value = Ember.makeArray(value);

            }

          }

          if (desc) {

            desc.set(this, keyName, value);

          } else {

            if (typeof this.setUnknownProperty === 'function' && !(keyName in this)) {

              this.setUnknownProperty(keyName, value);

            } else if (MANDATORY_SETTER) {

              Ember.defineProperty(this, keyName, null, value); // setup mandatory setter

            } else {

              this[keyName] = value;

            }

          }

        }

      }

    }

    finishPartial(this, m);

    delete m.proto;

    finishChains(this);

    this.init.apply(this, arguments);

  };

大段的代碼,看起來有些淩亂,就簡單的說明一下其含義吧。

這部分的代碼屬于class的預設構造方法,在new class()的時候被執行(這個時候已經有class的instance了,是以this會是類的執行個體)。

在這部分代碼中,主要的工作如下:

> 建立類執行個體的prototype;

> 合并類的mixins;

> 合并類的properties,包括構造properties的中繼資料和識别properties中綁定的描述;

> finishPartial中,會啟動binding的初始化,同步binding的值;

> 關閉prototype的合并;

> 調用類的執行個體的init方法;

雖然代碼量比較大,但是調理比較清晰,贊一個。

看到這裡,大家會比較奇怪,沒找到CoreObject上有extend方法啊?這部分代碼是托管在ClassMixin之中的。為了追根述源,我們繼續追蹤ClassMixin的代碼。

>>>>>>>>>>>>>Trace ClassMixin

到這裡,我們已經快到達分析ember.js的類的建立過程的終點了,還是從代碼開始:

var ClassMixin= Mixin.create({

  ClassMixin: Ember.required(),

  PrototypeMixin: Ember.required(),

  isClass: true,

  isMethod: false,

  extend: function() {

    var Class= makeCtor(), proto;

    Class.ClassMixin= Mixin.create(this.ClassMixin);

    Class.PrototypeMixin= Mixin.create(this.PrototypeMixin);

    Class.ClassMixin.ownerConstructor= Class;

    Class.PrototypeMixin.ownerConstructor= Class;

    reopen.apply(Class.PrototypeMixin, arguments);

    Class.superclass= this;

    Class.__super__  = this.prototype;

    proto= Class.prototype= o_create(this.prototype);

    proto.constructor= Class;

    generateGuid(proto, 'ember');

    meta(proto).proto = proto; // this will disable observers on prototype

    Class.ClassMixin.apply(Class);

    return Class;

  },

  createWithMixins: function() {

    var C = this;

    if (arguments.length>0) { this._initMixins(arguments); }

    return new C();

  },

  create: function() {

    var C = this;

    if (arguments.length>0) { this._initProperties(arguments); }

    return new C();

  },

  reopen: function() {

    this.willReopen();

    reopen.apply(this.PrototypeMixin, arguments);

    return this;

  },

  reopenClass: function() {

    reopen.apply(this.ClassMixin, arguments);

    applyMixin(this, arguments, false);

    return this;

  },

  detect: function(obj) {

    if ('function' !== typeof obj) { return false; }

    while(obj) {

      if (obj===this) { return true; }

      obj = obj.superclass;

    }

    return false;

  },

  detectInstance: function(obj) {

    return obj instanceof this;

  },

  metaForProperty: function(key) {

    var desc = meta(this.proto(), false).descs[key];

    Ember.assert("metaForProperty() could not find a computed property with key '"+key+"'.", !!desc && desc instanceof Ember.ComputedProperty);

    return desc._meta || {};

  },

  eachComputedProperty: function(callback, binding) {

    var proto = this.proto(),

        descs = meta(proto).descs,

        empty = {},

        property;

    for (var name in descs) {

      property = descs[name];

      if (property instanceof Ember.ComputedProperty) {

        callback.call(binding || this, name, property._meta || empty);

      }

    }

  }

});

ClassMixin.ownerConstructor= CoreObject;

CoreObject.ClassMixin = ClassMixin;

ClassMixin.apply(CoreObject);

老樣子,代碼還是幹貨,去掉了所有的注釋。

在ClassMixin的代碼中,我們看到了ember.js的大部分核心API,包括我們很關心的extend和create等類方法,這些方法通過Mixin的apply方法,合并到CoreObject上。

為了便于大家分析和研究,将相關的代碼貼上:

var MixinPrototype = Mixin.prototype;

MixinPrototype.apply= function(obj) {

  return applyMixin(obj, [this], false);

};

其實Mixin的合并一般是在class的建立的過程中來做的,不過對于ClassMixin來說,是需要将Mixin合并到CoreObject類型上,提供類型的方法和屬性,是以需要自己來觸發Mixin的合并過程。

再回到ClassMixin的extend方法,我們可以看到extend方法的核心使用makeCtor擷取一個class type,然後就是根據目前類型(就是super class)的ClassMixin和PrototypeMixin來構造class的ClassMixin和PrototypeMixin(會設定好其層級關系,包含super class的ClassMixin和PrototypeMixin),并設定其父子關系,填充中繼資料資訊,設定類基本的方法和屬性等等。

需要注意的是class上的ClassMixin和PrototypeMixin,分别表示類級别方法和執行個體級别方法的清單,以及在代碼中展現出來的reopen的概念,每次reopen之後,都會導緻class的PrototypeMixin增加一個層級,這是ember中this._supper()方法的底層的支援上的關鍵一步。

而ClassMixin的create方法更簡單,隻是設定類型的啟動參數(這裡是initProperties),并通過new來觸發其預設構造過程的執行。

到這裡,對于ember.js的繼承和對象的建立過程的分析就先告一段落了,代碼很多,但是如果從表達的語義來看是比較簡單的。考慮到ember.js基于原型法的繼承方式,以及其對觀察者模式的内在支援,可以了解ember.js為什麼會選擇這麼複雜的實作。

4、this._super()的實作方式

對ember.js的this._super()方法很喜歡,這在override類的執行個體方法的時候很實用。

前面寫的東東有點多,快到文檔字數的極限了,這部分我們就簡單點寫。

在ClassMixin的extend方法中,通過reopen(MixinPrototype.reopen)将class的PrototypeMixin準備就緒,之後在ClassMixin的create方法中,會觸發書寫在makeCtor方法中的類的預設的構造方法的執行。在其中,會調用makeCtor為class準備的proto方法,proto方法中将前面整理出來的class的PrototypeMixin的内容,使用Mixin的applyPartial方法(内部會調用applyMixin方法),将其合并到class的prototype之中。具體的代碼我就不給出來了,大家自己定位和閱讀吧。

在mixin的applyMixin方法中,會通過mergeMixin來合并所有Mixin提供的對象屬性(次序是自頂向下的次序,先基類(或稱為底層)的Mixin,後派生類(或稱為上層)的Mixin),合并的目标是提供的第一個參數,這裡是目前類的prototype。而父類的方法,在extend的過程中,會通過Mixin合并到目前class的PrototypeMixin之中。

很明顯,在合并mixin中資料的時候,會有一個次序,已經存在的方法被看成superMethod,而接下來合并的同名的方法需要得到他的引用。ember.js是通過Ember.wrap(method, superMethod)來擷取一個支援this._supre()的包裝函數的。 我們看看其代碼:

Ember.wrap= function(func, superFunc) {

  function K() {}

  function superWrapper() {

    var ret, sup = this._super;

    this._super= superFunc || K;

    ret = func.apply(this, arguments);

    this._super= sup;

    return ret;

  }

  superWrapper.wrappedFunction = func;

  superWrapper.__ember_observes__ = func.__ember_observes__;

  superWrapper.__ember_observesBefore__ = func.__ember_observesBefore__;

  return superWrapper;

};

這個包裝函數,将一個閉包方法傳回來,并引用調用時在調用之前設定this._super,并在調用完畢之後恢複,很聰明的解法。

5、對象屬性的get/set過程

ember.js在類執行個體的級别提供get/set方法,用于擷取和設定對象的資料。ember.js支援計算屬性,還支援觀察者的模式,是以其對屬性的管理要複雜一些。我們簡單的看看ember.js的屬性的讀寫過程,簡單的闡述一下其工作原理。

>>>>>>>>>get 過程:

get= function get(obj, keyName) {

  // Helpers that operate with 'this' within an #each

  if (keyName === '') {

    return obj;

  }

  if (!keyName && 'string'===typeof obj) {

    keyName = obj;

    obj = null;

  }

  Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined);

  if (obj === null || keyName.indexOf('.') !== -1) {

    return getPath(obj, keyName);

  }

  var meta = obj[META_KEY], desc = meta && meta.descs[keyName], ret;

  if (desc) {

    return desc.get(obj, keyName);

  } else {

    if (MANDATORY_SETTER&& meta && meta.watching[keyName] > 0) {

      ret = meta.values[keyName];

    } else {

      ret= obj[keyName];

    }

    if (ret === undefined &&

        'object' === typeof obj && !(keyName in obj) && 'function' === typeof obj.unknownProperty) {

      return obj.unknownProperty(keyName);

    }

    return ret;

  }

};

有幾個層次,分别描述一下:

> 如果keyName中存在'.',通過getPath做多路徑追蹤;

> 如果存在屬性的描述,使用屬性描述中的get方法擷取屬性資料;

> 否則,從對象上擷取資料。如果資料為undefind,并且在object上有unknownProperty方法,調用unknownProperty方法擷取屬性值;

>>>>>>>>>set 過程:

var set= function set(obj, keyName, value, tolerant) {

  if (typeof obj === 'string') {

    Ember.assert("Path '" + obj + "' must be global if no obj is given.", IS_GLOBAL.test(obj));

    value = keyName;

    keyName = obj;

    obj = null;

  }

  if (!obj || keyName.indexOf('.') !== -1) {

    return setPath(obj, keyName, value, tolerant);

  }

  Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined);

  Ember.assert('calling set on destroyed object', !obj.isDestroyed);

  var meta = obj[META_KEY], desc = meta && meta.descs[keyName],

      isUnknown, currentValue;

  if (desc) {

    desc.set(obj, keyName, value);

  } else {

    isUnknown = 'object' === typeof obj && !(keyName in obj);

    // setUnknownProperty is called if `obj` is an object,

    // the property does not already exist, and the

    // `setUnknownProperty` method exists on the object

    if (isUnknown && 'function' === typeof obj.setUnknownProperty) {

      obj.setUnknownProperty(keyName, value);

    } else if (meta&& meta.watching[keyName] > 0) {

      if (MANDATORY_SETTER) {

        currentValue= meta.values[keyName];

      } else {

        currentValue= obj[keyName];

      }

      // only trigger a change if the value has changed

      if (value !== currentValue) {

        Ember.propertyWillChange(obj, keyName);

        if (MANDATORY_SETTER) {

          if (currentValue === undefined && !(keyName in obj)) {

            Ember.defineProperty(obj, keyName, null, value); // setup mandatory setter

          } else {

            meta.values[keyName] = value;

          }

        } else {

          obj[keyName] = value;

        }

        Ember.propertyDidChange(obj, keyName);

      }

    } else {

      obj[keyName] = value;

    }

  }

  return value;

};

set方法考慮的東西要多一些,如下:

> 如果keyName中存在'.',通過setPath做路徑追蹤;

> 如果存在屬性的描述,通過其set方法設定屬性的值;

> 如果存在屬性的觀察者,擷取屬性的舊值,如果與新值不同,将屬性的資料存放到對象之中,并分别觸發屬性變更的前、後事件;

> 如果不存在觀察者,直接将屬性的值存放到對象上;

6、計算屬性的實作

有了前面描述的屬性描述的支援,對計算屬性的支援也就順理成章了,我們看看ember.js是怎麼做的。

先看看計算屬性的定義:

Person = Ember.Object.extend({

    // these will be supplied by `create`

    firstName: null,

    lastName: null,

    fullName: function() {

        var firstName = this.get('firstName');

        var lastName = this.get('lastName');

        return firstName + ' ' + lastName;

    }.property('firstName', 'lastName')

});

有點類似jquery的文法,寫一個函數,并調用函數的property方法傳回屬性的描述符。

我們看property的實作:

  Function.prototype.property= function() {

    var ret= Ember.computed(this);

    return ret.property.apply(ret, arguments);

  };

該方法中,通過Ember.computed方法産生一個ComputedProperty類的執行個體,并将參數設定到其property之中。

再看Ember.computed方法:

Ember.computed= function(func) {

  var args;

  if (arguments.length > 1) {

    args = a_slice.call(arguments, 0, -1);

    func = a_slice.call(arguments, -1)[0];

  }

  if ( typeof func !== "function" ) {

    throw new Error("Computed Property declared without a property function");

  }

  var cp = new ComputedProperty(func);

  if (args) {

    cp.property.apply(cp, args);

  }

  return cp;

};

注釋:

從Ember. computed的代碼來看,它支援向func中攜帶參數,如果有參數,最後一個參數是func,前面的是args。

Ember.computed很簡單,隻是建立一個ComputedProperty類的執行個體,并傳回它。

ComputedProperty類的代碼較多,我們隻貼一下必要的代碼:

function ComputedProperty(func, opts) {

  this.func = func;

  this._cacheable = (opts && opts.cacheable !== undefined) ? opts.cacheable : true;

  this._dependentKeys = opts && opts.dependentKeys;

  this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly);

}

Ember.ComputedProperty= ComputedProperty;

ComputedProperty.prototype= newEmber.Descriptor();

var ComputedPropertyPrototype= ComputedProperty.prototype;

ComputedPropertyPrototype.property= function() {

  var args = [];

  for (var i = 0, l = arguments.length; i < l; i++) {

    args.push(arguments[i]);

  }

  this._dependentKeys = args;

  return this;

};

ComputedProperty采用的是原型法的繼承方式,直接指定prototype為new Ember.Descriptor(),是以毫無疑問是Descriptor。其定義了Descriptor的get、set等方法,使用者控制屬性值的擷取和設定(代碼就不給了),還有提供了volatile和readOnly方法,用來控制計算屬性的結果是否緩存,以及是否運作set這個計算屬性的值。

六、小結

這裡,對ember.js的對象體系結構的描述該結束了,下面對上面描述的ember的核心功能做一個小小的彙總:

> 內建到Ember.Object上的觀察者的模式,允許觀察每個屬性值的變更;

> 通過Mixin提供類型無關的屬性和實作,支援AOP的設計模式;

> 提供extend、create等類方法,支援類的派生和類執行個體的建立;

> 提供reopen的模式,允許單個類在多處定義;

> 提供this._super()方法,可以通路到被override的方法;

> 提供屬性描述符(Ember.Descriptor),支援計算屬性、别名等複雜屬性;

> 對資料綁定的内在支援;

> 提供get/set方法(accessor),接管類的屬性值的設定&擷取請求;

我對ember.js展示的恢宏的體系結構非常欣賞,很喜歡ember.js的架構師設計ember的方式,也對ember源代碼中的優美實作感到很欣喜。讀好的代碼和讀一本好書的感覺類似,優秀的架構師給其注入靈魂和主線;而優美的代碼會讓每個細節充滿了美感。讀ember的代碼是很快樂的過程,希望ember将來會給我更多的驚喜。

ember,謝謝!

繼續閱讀