backbone源碼解讀

寫在前面

backbone是我兩年多前入門前端的時候接觸到的第一個框架,當初被backbone的強大功能所吸引(固然的確比裸寫js要好得多),雖然如今backbone並不算最主流的前端框架了,可是,它裏面大量設計模式的靈活運用,以及使人讚歎的處理技巧,仍是很是值得學習。我的認爲,讀懂老牌框架的源代碼比會用流行框架的API要有用的多。javascript

另外,backbone的源代碼最近也改了許多(特別是針對ES6),因此有些老舊的分析,可能會和如今的源代碼有些出入。css

因此我寫這一篇分析backbone的文章,供本身和你們一塊兒學習,本文適合使用過backbone的朋友,筆者水平有限,而內容又實有點多,不免會出差錯,歡迎你們在GitHub上指正html

接下來,咱們將經過一篇文章解析backbone,咱們是按照源碼的順序來說解的,這有利於你們邊看源代碼邊解讀,另外,我給源代碼加了所有的中文註釋和批註,請見這裏,強烈建議你們邊看源碼邊看解析,而且遇到我給出外鏈的地方,最好把外鏈的內容也看看(若是可以給你們幫助,歡迎給star鼓勵~)前端

固然,這篇文章很長[爲了不文章有上沒下,我仍是整合到一篇文章中了]。java

backbone宏觀解讀

backbone是很早期將MVC的思想帶入前端的框架,如今MVC以及後來的MVVM這麼火能夠在必定程度上歸功於backbone。關於前端MVC,我在本身的這篇文章中結合阮一峯老師的圖示簡單分析過,簡單來說就是Model層控制數據,View層經過發佈訂閱(在backbone中)來處理和用戶的交互,Controller是控制器,在這裏主要是指backbone的路由功能。這樣的設計很是直接清晰,有利於前端工程化。node

backbone中主要實現了Model、Collection、View、Router、History幾大功能,前四種咱們用的比較多,另外backbone基於發佈-訂閱模式本身實現了一套對象的事件系統Events,簡單來講Events可讓對象擁有事件能力,其定義了比較豐富的API,而且若是你引入了backbone,這套事件系統還能夠集成到本身的對象上,這是一個很是好的設計。jquery

另外,源代碼中全部的以_開頭的方法,能夠認爲是私有方法,是沒有必要直接使用的,也不建議用戶覆蓋。git

backbone模塊化處理、防止衝突和underscore混入

代碼首先進行了區分使用環境(self或者是global,前者表明瀏覽器環境(self和window等價),後者表明node環境)和模塊化處理操做,以後處理了在AMD和CommonJS加載規範下的引入方式,而且明確聲明瞭對jQuery(或者Zepto)和underscore的依賴。es6

很遺憾的是,雖然backbone這樣作了,可是backbone並不適合在node端直接使用,也不適合服務端渲染,另外還和ES6相處的不是很融洽,這個咱們後面還會陸續提到緣由。github

backbone noConflict

backbone也向jQuery致敬,學習了它的處理衝突的方式:

var previousBackbone = root.Backbone;
//...
Backbone.noConflict = function() {
    root.Backbone = previousBackbone;
    return this;
};

這段代碼的邏輯很是簡單,咱們能夠經過如下方式使用:

var localBackbone = Backbone.noConflict();   
var model = localBackbone.Model.extend(...);

混入underscore的方法

backbone經過addUnderscoreMethods將一些underscore的實用方法混入到本身定義的幾個類中(注:確切地說是可供構造調用的函數,咱們下文也會用類這個簡單明瞭的說法代替)。

這裏面值得一提的是關於underscore的方法(underscore的源碼解讀請移步這裏,fork from韓子遲),underscore的全部方法的參數序列都是固定的,也就是說第一個參數表明什麼第二個參數表明什麼,全部函數都是一致的,第一個參數必定表明目標對象,第二個參數必定表明做用函數(有的函數可能只有一個參數),在有三個參數的狀況下,第三個參數表明上下文this,另外若是有第四個參數,第三個參數表明初始值或者默認值,第四個參數表明上下文。因此addMethod就是根據以上規定來使用的。

另外關於javascript中的this,我曾經寫過博客在這裏,有興趣的能夠看

混入方法的實現邏輯:

var addMethod = function(length, method, attribute) {
  //... 
};
var addUnderscoreMethods = function(Class, methods, attribute) {
    _.each(methods, function(length, method) {
      if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
    });
};
//以後使用:
var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
      omit: 0, chain: 1, isEmpty: 1};
//混入一些underscore中經常使用的方法
addUnderscoreMethods(Model, modelMethods, 'attributes');

backbone Events

backbone的Events是一個對象,其中的方法(onlistenTooffstopListeningoncelistenToOncetrigger)都是對象方法。

整體上,backbone的Events實現了監聽/觸發/解除對本身對象自己的事件,也可讓一個對象監聽/解除監聽另一個對象的事件。

綁定對象自身的監聽事件on

關於對象自身事件的綁定,這個比較簡單,除了最基本的綁定以外(一個事件一個回調),backbone還支持如下兩種方式的綁定:

//傳統方式
model.on("change", common_callback);  

//傳入一個名稱,回調函數的對象
model.on({ 
     "change": on_change_callback,
     "remove": on_remove_callback
});  

//使用空格分割的多個事件名稱綁定到同一個回調函數上
model.on("change remove", common_callback);

這用到了它定義的一箇中間函數eventsApi,這個函數比較實用,能夠根據判斷使用的是哪一種方式(實際上這個判斷也比較簡單,根據傳入的是對象判斷屬於上述第二種方式,根據正則表達式判斷是上述的第三種方式,不然就是傳統的方式)。而後再進行遞歸或者循環或者直接處理。

在對象中存儲事件實際上大概是下述形式:

events:{
    change:[事件一,事件二]
    move:[事件一,事件二,事件三]
}

而其中的事件其實是一個整理好的對象,是以下形式:

{callback: callback, context: context, ctx: context || ctx, listening: listening}

這樣在觸發的時候,一個個調用就是了。

監聽其餘對象的事件listenTo

backbone還支持監聽其餘對象的事件,好比,B對象上面發生b事件的時候,通知A調用回調函數A.listenTo(B, 「b」, callback);,而這也是backbone處理很是巧妙的地方,咱們來看看它是怎麼作的。

實際上,這和B監聽本身的事件,而且在回調函數的時候把上下文變成A,是差很少的:B.on(「b」, callback, A);(on的第三個參數表明上下文)。

可是backbone還作了另外的事情,這裏咱們假設是A監聽B的一個事件(好比change事件好了)。

首先A有一個A._listeningTo屬性,這個屬性是一個對象,存放着它監聽的別的對象的信息A._listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0},這個id並非數字,是每個對象都有的惟一字符串,是經過_.uniqueId這個underscore方法生成的,這裏的obj是B,objId是B的_listenId,id是A的_listenId,count是一個計數功能,而這個A._listeningTo[id]會被直接引用賦值到上面事件對象的listening屬性中。

爲何要多listenTo?Inversion of Control

經過以上咱們彷佛有一個疑問,好像on就能把listenTo的功能搞定了,用一個listenTo純屬多餘,而且許多其餘的類庫也是隻有一個on方法。

首先,這裏會引入一個概念:控制反轉,所謂控制反轉,就是原來這個是B對象來控制的事件咱們如今交由A對象來控制,那如今假設A分別listenTo B、C、D三個對象,那麼這個時候假設A不監聽了,那麼咱們直接對A調用一個stopListening方法,則能夠同時解除對B、C、D的監聽(這裏我講的可能不是十分正確,這裏另外推薦一個文章)。

另外,咱們須要從backbone的設計初衷來看,backbone的重點是View、Model和Collection,實際上,backbone的View能夠對應一個或者多個Collection,固然咱們也可讓View直接對應Model,但問題是View也並不必定對應一個Model,可能對應多個Model,那麼這個時候咱們經過listenTo和stopListening能夠很是方便的添加、解除監聽。

//on的方式綁定
var view = {
    DoSomething :function(some){
       //...
    }
}
model.on('change:some',view.DoSomething,view);
model2.on('change:some',view.DoSomething,view);

//解綁,這個時候要作的事情比較多且亂
model.off('change:some',view.DoSomething,view);
model2.off('change:some',view.DoSomething,view);

//listenTo的方式綁定
view.listenTo(model,'change:some',view.DoSomething);
view.listenTo(model2,'change:some',view.DoSomething);

//解綁
view.stopListening();

另外,在實際使用中,listengTo的寫法也的確更加符合用戶的習慣.

如下是摘自backbone官方文檔的一些解釋,僅供參考:

The advantage of using this form, instead of other.on(event, callback, object), is that listenTo allows the object to keep track of the events, and they can be removed all at once later on. The callback will always be called with object as context.

解除綁定事件off、stopListening

與on不一樣,off的三個參數都是可選的

  • 若是沒有任何參數,off至關於把對應的_events對象總體清空

  • 若是有name參數可是沒有具體指定哪一個callback的時候,則把這個name(事件)對應的回調隊列所有清空

  • 若是還有進一步詳細的callback和context,那麼這個時候移除回調函數很是嚴格,必需要求上下文和原來函數徹底一致

off的最終實現函數是offApi,這個函數算上註釋有大概50行。

var offApi = function(events, name, callback, options) {
  //... 
}

這裏面須要單獨提一下,前面有這樣的幾行:

if (!name && !callback && !context) {
      var ids = _.keys(listeners);//全部監聽它的對應的屬性
      for (; i < ids.length; i++) {
        listening = listeners[ids[i]];
        delete listeners[listening.id];
        delete listening.listeningTo[listening.objId];
      }
      return;
}

這幾行是作了一件什麼事呢?
刪除了全部的多對象監聽事件記錄,以後刪除自身的監聽事件。咱們假設A監聽了B的一個事件,這個時候A._listenTo中就會多一個條目,存儲這個監聽事件的信息,而這個時候B的B._listeners也會多一個條目,存儲監聽事件的信息,注意這兩個條目都是按照id爲鍵的鍵值對來存儲,可是這個鍵是不同的,值都指向同一個對象,這裏刪除對這個對象的引用,以後就能夠被垃圾回收機制回收了。若是這個時候調用B.off(),那麼這個時候,以上的兩個條目都被刪除了。另外,注意最後的return,以及Events.off中的:

this._events = eventsApi(offApi, this._events, name, callback, {
      context: context,
      listeners: this._listeners
});

因此若是B.off()這樣調用而後直接把 B._events 在以後也清空了,太巧妙了

以後有一個對names(事件名)的循環(若是沒有指定,那麼默認就是全部names),這個循環內容理解起來比較簡單,裏面也順便照顧了_listeners_listenTo這些變量。這裏不過多解釋了。

另外,stopListening實際上也是調用offApi,先處理了一下交給off函數,這也是設計模式運用典範(適配器模式)。

once和listenToOnce

這兩個函數顧名思義,和on以及listenTo的區別不大,惟一的區別就是回調函數只供調用一次,多觸發調用也沒有用(實際上不會被觸發了)。

二者都用到了onceMap這個函數,咱們分析一下這個函數:

var onceMap = function(map, name, callback, offer) {
    if (callback) {
      //_.once:建立一個只能調用一次的函數。重複調用改進的方法也沒有效果,只會返回第一次執行時的結果。 做爲初始化函數使用時很是有用, 不用再設一個boolean值來檢查是否已經初始化完成.
      var once = map[name] = _.once(function() {
        offer(name, once);
        callback.apply(this, arguments);
      });
      //這個在解綁的時候有一個分辨效果
      once._callback = callback;
    }
    return map;
 };

backbone的設計思路是這樣的:用_.once()建立一個只能被調用一次的函數,這個函數在第一次被觸發調用的時候,進行解除綁定(offer其實是一個已經綁定好this的解除綁定函數,這個能夠參見once和listenToOnce的源代碼),而後再調用callback,這樣既實現了調用一次的目的,也方便了垃圾回收。

其餘和on以及listenTo的時候同樣,這裏就不過多介紹了。

trigger

trigger函數是用於觸發事件,支持多個參數,除了第一個參數之外,其餘的參數會依次放入觸發事件的回調函數的參數中(backbone默認對3個參數及如下的狀況下進行call調用,這種處理方式緣由之一是call調用比apply調用的效率更高從而優先使用(關於call和apply的性能對比:https://jsperf.com/call-apply...),另一方面源碼中並無超過三個參數的狀況,因此用call支持到了三個參數,其他狀況採用性能較差可是寫起來方便的apply)。

另外值得一提的是,Events支持all事件,即若是你監聽了all事件,那麼任何事件的觸發都會調用all事件的回調函數列。

關於trigger部分的源代碼比較簡單,而且我也增長了一些評註,這裏就不貼代碼了。

context 和 ctx

有心的朋友也許注意到,backbone在事件中用到了context和ctx這兩個"貌似"表示當前上下文的對象,而且在若是有context的狀況下,這兩個幾乎同樣:

handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});

這裏我根據本身的理解,儘可能解釋一下。

咱們能夠主要看off方法及trigger方法,咱們發現上面兩屬性在這兩個方法中分別被使用了。

off裏須要對context進行比較決定是否要刪除對應的事件,因此model._events中保存下來的context,必須是未作修改的。

而trigger裏在執行回調函數時,須要指定其做用域,當綁定事件時沒有給定做用域,則會使用被監聽的對象當回調函數的做用域。

實際上,我以爲這個ctx有點多餘,咱們徹底能夠在trigger中這樣寫:

(ev = events[i]).callback.call(ev.context || ev.obj)

backbone Model

backbone的Model其實是一個可供構造調用的函數,backbone採用污染原型的方式把定義好的屬性都定義在了prototype上,這可能並非一個很是穩當的作法,可是在backbone中這樣作倒是沒有什麼不能夠的,這個咱們在以後講extend方法的時候會進行補充。

咱們先看看這個函數在實例化的時候會作點什麼:

var Model = Backbone.Model = function(attributes, options) {
    var attrs = attributes || {};
    options || (options = {});
    //這個preinitialize函數其實是爲空的,能夠給有興趣的開發者重寫這個函數,在初始化Model以前調用
    this.preinitialize.apply(this, arguments);
    //Model的惟一的id
    this.cid = _.uniqueId(this.cidPrefix);
    this.attributes = {};
    if (options.collection) this.collection = options.collection;
    //若是以後new的時候傳入的是JSON,咱們必須在options選項中聲明parse爲true
    if (options.parse) attrs = this.parse(attrs, options) || {};
    //_.result:若是指定的property的值是一個函數,那麼將在object上下文內調用它;不然,返回它。若是提供默認值,而且屬性不存在,那麼默認值將被返回。若是設置defaultValue是一個函數,它的結果將被返回。
    //這裏調用_.result至關於給出了餘地,本身寫defaults的時候能夠直接寫一個對象,也能夠寫一個函數,經過return一個對象的方式把屬性包含進去
    var defaults = _.result(this, 'defaults');
    //defaults應該是在Backbone.Model.extends的時候由用戶添加的,用defaults對象填充object 中的undefined屬性。 而且返回這個object。一旦這個屬性被填充,再使用defaults方法將不會有任何效果。
    attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
    this.set(attrs, options);
    //存儲歷史變化記錄
    this.changed = {};
    //這個initialize也是空的,給初始化以後調用
    this.initialize.apply(this, arguments);
};

咱們能夠看出,this.attributes是存儲實際內容的。

另外,preinitialize和initialize不只在Model中有,在以後的Collection、View和Router中也都出現了,一個是在初始化前調用,另一個是在初始化以後調用。

關於preinitialize的問題,咱們後文還要繼續討論,它的出現和ES6有關。

Model set

Model的set方法是一個重點的方法,這個方法的功能比較多,自己甚至還能夠刪除屬性,由於unset內部和clear的內部等也調用了set方法。在用戶手動賦值的時候,支持下面兩種賦值方式:"key", value{key: value}兩種賦值方式。

咱們分析這個函數總共作了哪些事情:

  • 對兩種賦值方式的支持"key", value{key: value}的預處理。

  • 若是你寫了validate驗證函數沒有經過驗證,那麼就不繼續作了(須要顯式聲明使用validate)。

  • 進行變量的更改或者刪除,順便把歷史版本的問題解決掉。

  • 若是不是靜默set的,那麼這個時候開始進行change事件的觸發。

具體這一塊註釋筆者寫的很是詳細,因此在這裏也再也不贅述。

fetch、save、destroy

這幾個功能是須要跟服務端交互的,因此咱們放在一塊兒來分析一下。

backbone經過封裝好模型和服務器交互的函數,大大方便了開發者和服務端數據同步的工做,固然,這須要一個對應的後端,不只須要支持POST、PUT、PATCH、DELETE、GET多種請求,甚至連url的格式都給定義好了,url的格式爲:yourUrl/id,這個id確定是須要咱們傳入的,而且要求跟服務器上的id對應(畢竟服務器要識別處理)

注意:url並不必定非要按照backbone的來,咱們徹底能夠調用這幾個方法的時候再指定一個url{url:myurl,success:successFunction},這個部分backbone 在sync函數中進行了一個判斷處理,優先選擇後指定的url,不過這樣對咱們來講是比較麻煩的,也並不符合backbone的設計初衷

這三個函數最後都用到了sync函數,因此咱們要先分析sync函數:

Backbone.sync = function(method, model, options) {
  //...
};
  
Backbone.ajax = function() {
  return Backbone.$.ajax.apply(Backbone.$, arguments);
};

sync函數在其中調用了ajax函數,而ajax函數就是jQuery的ajax,這個咱們很是熟悉,它能夠插入很是多的參數,咱們能夠這裏查看文檔。

另外,這個sync支持兩個特殊狀況:

  • emulateHTTP:若是你想在不支持Backbone的默認REST/ HTTP方式的Web服務器上工做, 您能夠選擇開啓Backbone.emulateHTTP。 設置該選項將經過 POST 方法僞造 PUT,PATCH 和 DELETE 請求 用真實的方法設定X-HTTP-Method-Override頭信息。 若是支持emulateJSON,此時該請求會向服務器傳入名爲 _method 的參數。

  • emulateJSON:若是你想在不支持發送 application/json 編碼請求的Web服務器上工做,設置Backbone.emulateJSON = true;將致使JSON根據模型參數進行序列化, 並經過application/x-www-form-urlencoded MIME類型來發送一個僞造HTML表單請求

具體的這個sync方法,就是構造ajax參數的過程。

fetch

fetch能夠傳入一個回調函數,這個回調函數會在ajax的回調函數中被調用,另外ajax的回調函數是在fetch中定義的,這個回調函數作了這樣幾件事情:

options.success = function(resp) {
        //處理返回數據
        var serverAttrs = options.parse ? model.parse(resp, options) : resp;
        //根據服務器返回數據設置模型屬性
        if (!model.set(serverAttrs, options)) return false;
        //觸發自定義回調函數
        if (success) success.call(options.context, model, resp, options);
        //觸發事件
        model.trigger('sync', model, resp, options);
 };
save

save方法爲向服務器提交保存數據的請求,若是是第一次保存,那麼就是POST請求,若是不是第一次保存數據,那麼就是PUT請求。

其中,傳遞的options中可使用的字段以及意義爲:

  • wait: 能夠指定是否等待服務端的返回結果再更新model。默認狀況下不等待

  • url: 能夠覆蓋掉backbone默認使用的url格式

  • attrs: 能夠指定保存到服務端的字段有哪些,配合options.patch能夠產生PATCH對模型進行部分更新

  • patch:boolean 指定使用部分更新的REST接口

  • success: 本身定義一個回調函數

  • data: 會被直接傳遞給jquery的ajax中的data,可以覆蓋backbone全部的對上傳的數據控制的行爲

  • 其餘: options中的任何參數都將直接傳遞給jquery的ajax,做爲其options

關於save函數具體的處理邏輯,我在源代碼中添加了很是詳細的註釋,這裏就不展開了。

destroy

銷燬這個模型,咱們能夠分析,銷燬模型要作如下幾件事情:

  • 中止對該對象全部的事件監聽,自己都沒有了,還監聽什麼事件

  • 告知服務器本身要被銷燬了(若是isNew()返回true,那麼其實不用向服務器發送請求)

  • 若是它屬於某一個collection,那麼要告知這個collection要把這個模型移除

其中,傳遞的options中可使用的字段以及意義爲:

  • wait: 能夠指定是否等待服務端的返回結果再銷燬。默認狀況下不等待

  • success: 本身定義一個回調函數

Model的其餘內容

另外值得一提的是,Model是要求傳入的id惟一的,可是對這個id若是重複的狀況下的錯誤處理作的不是很到位,因此有的時候你看控制檯報錯並不能及時發現問題。

backbone Collection

Collection也是一個可供構造調用的函數,咱們仍是先看看這個Collection作了些什麼:

var Collection = Backbone.Collection = function(models, options) {
    options || (options = {});
    this.preinitialize.apply(this, arguments);
    //實際上咱們在建立集合類的時候大多數都會定義一個model, 而不是在初始化的時候從options中指定model
    if (options.model) this.model = options.model;
    //咱們能夠在options中指定一個comparator做爲排序器
    if (options.comparator !== void 0) this.comparator = options.comparator;
    //_reset用於初始化
    this._reset();
    this.initialize.apply(this, arguments);
    //若是咱們在new構造調用的時候聲明瞭models,這個時候須要調用reset函數
    if (models) this.reset(models, _.extend({silent: true}, options));
  };

實際上,我以爲backbone的Model、View、Collection裏的邏輯仍是比較清楚的,可讀性也比較強,因此主要就是把註釋寫在代碼裏面。

Collection set

collection的一個核心方法,內容很長,咱們能夠把它理解爲重置:給定一組新的模型,增長新的,去除不在這裏面的(在添加模式下不去除),混合已經存在的。可是這個方法同時也很靈活,能夠經過參數的設定來改變模式

set可能有以下幾個調用場景:

  1. 重置模式,這個時候不在models裏的model都會被清除掉。對應上文的:var setOptions = {add: true, remove: true, merge: true};

  2. 添加模式,這個時候models裏的內容會作添加用,若是有重複的(cid來判斷),會覆蓋。對應上文的:var addOptions = {add: true, remove: false};

咱們仍是理一理裏面作了哪些事情:

  • 先規範化models和options兩個參數

  • 遍歷models:

    • 若是是重置模式,那麼遇到重複的就直接覆蓋掉,而且也添加到set隊列,遇到新的就先添加到set隊列。以後還要刪除掉models裏沒有而原來collection裏面有的

    • 若是是添加模式,那麼遇到重複的,就先添加到set隊列,遇到新的也是添加到set隊列

  • 以後進行整理,整合到collection中(可能會觸發排序操做)

  • 若是不是靜默處理,這個時候會觸發各種事件

固然,咱們在進行調用的時候,是不須要考慮這麼複雜的,這個函數之因此作的這麼複雜,是由於它也供許多內置的其餘函數調用了,這樣能夠減小重複代碼的冗餘,符合函數式編程的思想。另外set函數雖然繁雜卻不贅餘,裏面定義的函數內變量邏輯都有本身的做用。

sort

上文中提到了sort函數,sort所依據的是用戶傳入的comparator參數,這個參數能夠是一個字符串表示的單個屬性也能夠是一個函數,另外也能夠是一個多個屬性組成的數組,若是是單個屬性或者函數,就調用underscore的排序方法,若是是一個多個屬性組成的數組,就調用原生的數組排序方法(原生方法支持按照多個屬性分優先級進行排序)

fetch、create

這是Collection中涉及到和服務端交互的方法,這兩個方法很是有區別。

fetch是直接從服務器拉取數據,並無調用model的fetch方法,返回的數據格式應當是直接能夠調用上文的set函數的數據格式,另外值得注意的是,想要調用這個方法,必定要先指定url

create是指將特定的model上傳到服務器上去,並無調用本身的方法而是最後調用了model自身的方法model.save(null, options),這裏第一個參數被賦值成null仍是有意義的,咱們經過分析save函數前幾行代碼就能夠很明顯地分析出緣由。

CollectionIterator

這是一個基於ES6的新的內容,目的是建立一個遍歷器,以後,咱們能夠在collection的一些方法中運用這個可遍歷對象。

這個方面的知識能夠看這裏補充,三言兩語也沒法說清,簡單地講,就是若是正確地定義了一個next屬性方法,這個對象就能夠按照本身定義的方式來遍歷了。

而backbone這裏定義的這個遍歷器更增強大,能夠分別按照key、value、key和value三種方式遍歷

我這裏給出一個使用方式:

window.Test = Backbone.Model.extend({
    defaults: {content: ''
    }
});
// 建立集合模型類  
window.TestList = Backbone.Collection.extend({
    model: Test
});
// 向模型添加數據
var data = new TestList(
        [
            {
                id:100,
                content: 'hello,backbone!'
            },
            {
                id:101,
                content: 'hello,Xiaotao!'
            }
        ]
);
for(var ii of data.keys()){
    console.log(ii);
}
for( ii of data.values()){
    console.log(ii);
}
for( ii of data.entries()){
    console.log(ii);
}

具體這裏是如何實現的,我相信你們看了上文連接給出的擴展知識以後,而後再結合我寫了註釋的源代碼,應該都能看懂了。

Collection其餘內容

另外,Collection還實現了很是多的小方法,也混入了不少underscore的方法,但核心都是操做this.modelsthis.models是一個正常的數組(因此,在js中自己實現了的方法也是能夠在這裏使用的),能夠直接訪問。

另外值得一提的是,Collection中有一個_byId變量,這個變量經過cid和id來存取,起到一個方便直接存取的做用,在某些時候很是方便。

_addReference: function(model, options) {
      this._byId[model.cid] = model;
      var id = this.modelId(model.attributes);
      if (id != null) this._byId[id] = model;
      model.on('all', this._onModelEvent, this);
},

另外實際上,model除了做爲Collection裏面的元素,而且經過一個collection屬性指向對應的Collection,實際上聯繫也並非很是多,這也比較符合低耦合高內聚的策略。

backbone View

接下來咱們進入backbone的View部分,也就是和用戶打交道的部分,我一開始用backbone的時候就是被View層能夠經過定義events對象數組來方便地進行事件管理所吸引(雖然如今看來還有更方便的方案)

咱們先來看一下View函數在用戶新建View的時候作了些什麼:

var View = Backbone.View = function(options) {
    this.cid = _.uniqueId('view');
    this.preinitialize.apply(this, arguments);
    //_.pick(object, *keys):返回一個object副本,只過濾出keys(有效的鍵組成的數組)參數指定的屬性值。或者接受一個判斷函數,指定挑選哪一個key。
    _.extend(this, _.pick(options, viewOptions));
    //初始化dom元素和jQuery元素工做
    this._ensureElement();
    //自定義初始化函數
    this.initialize.apply(this, arguments);
};

這裏面值得一提的是this._ensureElement()這個函數,這個函數內部調用了不少函數,作了不少工做,咱們首先看這個函數:

_ensureElement: function() {
      if (!this.el) {
        var attrs = _.extend({}, _.result(this, 'attributes'));
        if (this.id) attrs.id = _.result(this, 'id');
        if (this.className) attrs['class'] = _.result(this, 'className');
        this.setElement(this._createElement(_.result(this, 'tagName')));
        this._setAttributes(attrs);
      } else {
        this.setElement(_.result(this, 'el'));
      }
},

根據你是否傳入一個dom元素(這個dom元素用來和View對應,也能夠是jQuery元素)分紅了兩種狀況執行,咱們先看不傳入的狀況:

這個時候咱們能夠定義一些屬性,這些屬性都在接下來賦值到生成的dom對象上:

_setAttributes: function(attributes) {
      this.$el.attr(attributes);
}

接下來看假設傳入了了的狀況:

setElement: function(element) {
      this.undelegateEvents();
      this._setElement(element);
      this.delegateEvents();
      return this;
},

這裏面又調用了三個函數,咱們看一下這三個函數:

undelegateEvents: function() {
      if (this.$el) this.$el.off('.delegateEvents' + this.cid);
      return this;
},

_setElement: function(el) {
      this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
      this.el = this.$el[0];
},

delegateEvents: function(events) {
      events || (events = _.result(this, 'events'));
      if (!events) return this;
      this.undelegateEvents();
      for (var key in events) {
        var method = events[key];
        if (!_.isFunction(method)) method = this[method];
        if (!method) continue;
        var match = key.match(delegateEventSplitter);
        this.delegate(match[1], match[2], _.bind(method, this));
    }
    return this;
},

delegate: function(eventName, selector, listener) {
      this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
      return this;
},

上面第四個函數爲第三個函數所調用的,所以咱們放在了一塊兒。

第一個函數是解綁backbone所用的jQuery事件命名空間下的事件(.delegateEvents),這個是方式這個事件被以前的其餘View使用過,從而形成污染(實際上,這個通常狀況下用的是很少的)。

第二個函數是初始化dom對象和jQuery對象,$el表明jQuery對象,el表明dom對象。

第三個函數是把咱們寫的監聽事件進行從新綁定,咱們寫的事件知足下面的格式:

//舉個例子: 
 {
     'mousedown .title':  'edit',
     'click .button':     'save',
     'click .open':       function(e) { ... }
 }

上面第三個函數就是一個解析函數,解析好後直接調用delegate函數進行事件的綁定,這裏要注意你定義的事件的元素必須在提供的el內的,不然沒法訪問到。

render

另外,backbone中有一個render函數:

render: function() {
      return this;
},

這個render函數實際上有比較深遠的意義,render函數默認是沒有操做的,咱們能夠本身定義操做,而後能夠在事件中'change' 'render'這樣對應,這樣每次變化就會從新調用render重繪,咱們也能夠自定義好render函數而且在初始化函數initialize中調用。另外,render函數默認的return this;隱含了backbone的一種指望:返回this從而支持鏈式調用。

render可使用underscore的模版,而且這也是推薦作法,如下是一個很是簡單的demo:

var Bookmark = Backbone.View.extend({
  template: _.template(...),
  render: function() {
    this.$el.html(this.template(this.model.attributes));
    return this;
  }
});

backbone router、history

router

backbone相比於一些流行框架的好處就是本身實現了router部分,不用再引入其餘插件,這點十分方便。

咱們在使用router的時候,一般會採用以下寫法:

var Workspace = Backbone.Router.extend({

  routes: {
    "help":                 "help",    // #help
    "search/:query":        "search",  // #search/kiwis
    "search/:query/p:page": "search"   // #search/kiwis/p7
  },

  help: function() {
    ...
  },

  search: function(query, page) {
    ...
  }

});

router的供構造調用的函數的主體部分也至關簡單,沒有作多餘的事情:

var Router = Backbone.Router = function(options) {
    options || (options = {});
    this.preinitialize.apply(this, arguments);
    //注意這個地方,options的routes會直接this的routes,因此若是在創建類的時候指定routes,實例化的時候又擴展了routes,是會被覆蓋的
    if (options.routes) this.routes = options.routes;
    //對本身定義的路由進行處理
    this._bindRoutes();
    //調用自定義初始化函數
    this.initialize.apply(this, arguments);
};

這裏咱們展開_bindRoutes:

_bindRoutes: function() {
      if (!this.routes) return;
      this.routes = _.result(this, 'routes');
      var route, routes = _.keys(this.routes);
      while ((route = routes.pop()) != null) {
        this.route(route, this.routes[route]);
      }
},

route函數是把路由處理成正則表達式形式,而後調用history.route函數進行綁定,history.route函數在網址每次變化的時候都會檢查匹配,若是有匹配就執行回調函數,也就是下文Backbone.history.route傳入的第二個參數,這樣路由部分和history部分就聯繫在一塊兒了。

route: function(route, name, callback) {
      //若是不是正則表達式,轉換之
      if (!_.isRegExp(route)) route = this._routeToRegExp(route);
      if (_.isFunction(name)) {
        callback = name;
        name = '';
      }
      if (!callback) callback = this[name];
      var router = this;
      Backbone.history.route(route, function(fragment) {
        var args = router._extractParameters(route, fragment);
        if (router.execute(callback, args, name) !== false) {
          router.trigger.apply(router, ['route:' + name].concat(args));
          router.trigger('route', name, args);
          Backbone.history.trigger('route', router, name, args);
        }
      });
      return this;
},

上面的這段代碼首先可能會調用_routeToRegExp這個函數進行正則處理,這個函數多是backbone中最難懂的函數,不過不懂也並不影響咱們繼續分析(實際上,筆者也並無徹底懂這個函數,因此但願經驗人士能夠在這裏給予幫助)。

_routeToRegExp: function(route) {
      route = route.replace(escapeRegExp, '\\$&')//這個匹配的目的是將正則表達式字符進行轉義
                   .replace(optionalParam, '(?:$1)?')
                   .replace(namedParam, function(match, optional) {
                     return optional ? match : '([^/?]+)';
                   })
                   .replace(splatParam, '([^?]*?)');
      return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
},

另外調用了_extractParameters這個函數和router.execute這個函數,前者的做用就是將匹配成功的URL中蘊含的參數轉化成一個數組返回,後者接受三個參數,分別是回調函數,參數列表和函數名(這裏以前只有兩個函數,後來backbone增長了第三個參數)。

_extractParameters: function(route, fragment) {
      var params = route.exec(fragment).slice(1);
      return _.map(params, function(param, i) {
        // Don't decode the search params.
        if (i === params.length - 1) return param || null;
        return param ? decodeURIComponent(param) : null;
      });
}
execute: function(callback, args, name) {
      if (callback) callback.apply(this, args);
},

router的內容也就這些了,實現的比較簡單清爽,代碼也很少,關於處理歷史記錄瀏覽器兼容性的問題都放在了history部分,因此接下來咱們來分析難啃的history部分。

history

這一塊的內容比較重要,而且相比於以前的內容有些複雜,我儘可能把本身的理解全都講解出來。

咱們先說明一下這個歷史記錄的做用:
當你在瀏覽器訪問的時候,能夠經過左上角的前進後退進行切換,這就是由於產生了歷史記錄。

那麼什麼方式能夠產生歷史記錄呢?

  1. 頁面跳轉(確定的,可是並不適用於SPA)

  2. hash變化:形如<a href="#123"></a>這種點擊後會觸發歷史記錄),可是不幸的是在IE7下並不能被寫入歷史記錄(雖然筆者是對IE9如下堅定說不的)

  3. pushState,這種比較牛逼,能夠默默的改變路由,好比把article.html#article/54改爲article.html#article/53可是不觸發頁面的刷新,由於通常狀況下這算是兩個頁面的,另外,這種狀況須要服務端的支持,所以我在用backbone的時候較少採用這種作法(如今有一個概念叫作pjax,就是ajax+pushState,具體能夠Google之)

  4. iframe內url變化,變化iframe內的url也會觸發歷史記錄,可是這個比較麻煩,另外,在IE中,不管iframe是一開始靜態寫在html中的仍是後來用js動態建立的,均可以被寫入瀏覽器的歷史記錄,其餘瀏覽器通常只支持靜態寫在html中。因此,咱們通常在2&3都不可用的狀況下,才選用這種狀況(IE7如下)

以上講的基本就是backbone使用的方式,接下來咱們再按照backbone使用邏輯和優先級進行一些講解:

backbone默認是使用hash的,在不支持hash的瀏覽器中使用iframe,若是想要使用pushState,須要顯式聲明而且瀏覽器自己要支持(若是使用了pushState的話hash就不用了)。

因此backbone的history有一個很是大的start函數,這個函數從頭至尾作了以下幾件事情:

  • 將頁面的根部分保存在root中,默認是/

  • 判斷是否想用hashChange(默認爲true)以及支持與否,判斷是否想用pushState以及支持與否。

  • 判斷一下究竟是用hash仍是用push,而且作一些url處理

  • 若是須要用到iframe,這個時候初始化一下iframe

  • 初始化監聽事件:用hash的話能夠監聽hashchange事件,用pushState的話能夠監聽popState事件,若是用了iframe,沒辦法,只能輪詢了,這個主要是用來用戶的前進後退。

  • 最後最重要的:先處理如下當前頁面的路由,也就是說,假設用戶直接訪問的並非根頁面,不能什麼也不作呀,要調用相關路由對應的函數,因此這裏要調用loadUrl

和start對應的stop函數,主要作了一些清理工做,若是能讀懂start,那麼stop函數應該是不難讀懂的。

另外還有一個比較長的函數是navigate,這個函數的做用主要是存儲/更新歷史記錄,主要和瀏覽器打交道,若是用hash的話,backbone自身是不會調用這個函數的(由於用不到),可是能夠供開發者調用:

開發者能夠經過這個函數用js代碼自動管理路由:

openPage: function(pageNumber) {
  this.document.pages.at(pageNumber).open();
  this.navigate("page/" + pageNumber);
}

另外,backbone在這一部分定義了一系列工具函數,用於處理url。

backbone的history這一部分寫的很是的優秀,兼容性也很是的高,而且充分知足了高聚合低耦合的特色,若是本身也要實現history管理這一部分,那麼backbone的這個history絕對是一個優秀的範例。

extend

最後,backbone還定義了一個extend函數,這個函數咱們再熟悉不過了,不過它的寫法並無咱們想象的那麼簡單,

這個函數並無直接將屬性assign到parent上面(this),是由於這樣會產生一個顯著的問題:污染原型
因此實際上backbone的作法是新建了一個子類,這個子對象承擔着全部內容.

另外,這個extend函數也借鑑了ES6的一些寫法,內容很少,理解起來也是簡單的。

ES6&backbone

backbone支持ES6的寫法,關於這個寫法問題,曾經GitHub上面有過激烈的爭論,這裏我稍做總結,先給出一個目前可行的寫法:

class DocumentRow extends Backbone.View {

    preinitialize() {
        _.extend(this, {
          tagName:  "li",
          className: "document-row",
          events: {
            "click .icon":          "open",
            "click .button.edit":   "openEditDialog",
            "click .button.delete": "destroy"
          }
        });
    }

    initialize() {
        this.listenTo(this.model, "change", this.render);
    }

    render() {
        //...
    }
}

實際上,這個問題出現以前backbone的源代碼中是沒有preinitialize函數的,關於爲何最終是這樣,我總結如下幾點:

  • ES6的class不能直接寫屬性(直接報錯),都要寫成函數,由於若是有屬性的話會出現共享屬性的問題。

  • ES6的class寫法和ES5的不同,也和backbone本身定義的extend是不同的。是先要調用父類的構造方法,而後再有子類的this,在調用constructor以前是沒法使用this的。因此下面這種寫法就不行了:

class DocumentRow extends Backbone.View {

    constructor() {
        this.tagName =  "li";
        this.className = "document-row";
        this.events = {
            "click .icon":          "open",
            "click .button.edit":   "openEditDialog",
            "click .button.delete": "destroy"
        };
        super();
    }

    initialize() {
        this.listenTo(this.model, "change", this.render);
    }

    render() {
        //...
    }
}

可是若是把super提早,那麼這個時候tagName什麼的尚未賦值呢,element就已經創建好了。

另外,把屬性強制寫成函數的作法是被backbone支持的,可是我相信沒有多少人願意這樣作吧:

class DocumentRow extends Backbone.View {

    tagName() { return "li"; }

    className() { return "document-row";}

    events() {
        return {
            "click .icon":          "open",
            "click .button.edit":   "openEditDialog",
            "click .button.delete": "destroy"
        };
    }

    initialize() {
        this.listenTo(this.model, "change", this.render);
    }

    render() {
        //...
    }
}

因此咱們須要:及早把一些屬性賦給父類覆蓋掉父類默認屬性,而後調用父類構造函數,而後再調用子類構造函數。因此加入一個preinitialize方法是一個比較好的選擇。

若是尚未理解,不妨看看下面這個本質等價的小例子:

class A{
    constructor(){
        this.s=1;
        this.preinit();
        this.dosomething();
        this.init();
    }
    preinit(){}
    init(){}
    dosomething(){console.log("dosomething:",this.s)}//dosomething 2
}
class B extends A{
    preinit(){this.s=2;}
    init(){}
}
var b1 = new B();
console.log(b1.s);//2

總結

通過以上漫長的對backbone源代碼分析的過程,咱們瞭解了一個優秀的框架的源代碼,我總結了backbone源碼的幾個特色以下:

  • 充分發揮函數式編程的精神,符合函數式編程,以前有位前輩說對js的運用程度就取決於對js的函數式編程的認識程度,也是不無道理的。

  • 高內聚低耦合可擴展,這一方面方便了咱們使用backbone的一部份內容(好比只使用Events或者router),另一方面也方便了插件開發,以及能和其餘的庫比較好的兼容,我認爲,這並非一個強主張的庫,你能夠小規模地按照本身的方式使用,也能夠大規模的徹底按照backbone的指望使用。

  • 在使用和兼容ES6的新特性上作了很多努力,在源代碼中好幾處都體現了ES6的內容,這讓backbone做爲一個老牌框架,在現在大規模使用作網頁應用,依然十分可行。

缺點:

  • backbone嚴重依賴jQuery和underscore,這對backbone起到了牽制做用,假設jQuery或者underscore改變了一個方法或者一個接口,那麼backbone也要跟着改,另外backbone依賴的jQuery和underscore也有一些限制,直接隨便引入這三個文件極可能就會報錯(通常狀況下都引入最新的是沒有問題的),這是backbone比較很差的一個地方(要否則自身也不可能作到這麼輕量級)

--

參考資料
backbone官方文檔:http://backbonejs.org/
backbone中文文檔:http://www.css88.com/doc/back...
Why Backbone.js and ES6 Classes Don't Mix:http://benmccormick.org/2015/...

關於backbone&ES6的討論:
https://github.com/jashkenas/...
https://github.com/jashkenas/...

相關文章
相關標籤/搜索