博客 / 詳情

返回

Backbone源碼解讀(二)

1. 開場

強烈建議一邊看着源碼一邊讀本文章,本文不貼大段代碼。源碼地址。
在寫backbone應用的時候,説實話,大部分的時間都是在寫這三個模塊的內容。關於這三個模塊的分析網上隨隨便便就能找到一堆還不錯的文章。但我希望能夠找到一條線索,能把各自模塊的內部機理整理清楚。就像前一篇文章中介紹的Events那樣。Events整個模塊其實就是通過一些外部的方法來修改內部對象的屬性,從而達到事件管理的目的。以一條線索來看待整個模塊,一切都清晰瞭然了。下面就開始了~

(最近要開學,要準備回學校上課,亂七八糟的東西很多,所以文章可能也會拖一陣子啦...但是我還是非常希望能夠寫下來,半途而廢的感覺真心不好...

這一篇文章主要講backboneModel, CollectionView。這三個模塊有很多相似的地方。這篇文章不會把模塊的每一個方法都介紹一遍,因為只要看源碼就知道,其實主要的方法只有幾個,而很多其他的模塊實際上只是在調用這幾個核心的方法而已。

2. Model & Collection & View

首先講一下三者的相似之處。這一節讓我們來看看這三個模塊一個總體結構。
這三個模塊在結構上和Events不同。他們先通過以下方式來定義構造函數。(以View為例)

var View = Backbone.View = function(options) {
    // 構造函數的內容
};

構造函數的內部一般會做以下幾個操作:

  • 各種給內部對象設置屬性。(各種this.a = b

  • 調用preinitialize

this.preinitialize.apply(this, arguments);
  • 調用initialize

this.initialize.apply(this, arguments);

各個模塊的方法和屬性是通過underscoreextend來獲得的。注意在extend新加入的方法和屬性中,以下劃線開頭的變量是內部函數名。(其實理論上用户也可以調用這些方法,誰叫Javascript沒有內部變量呢...)這些內部方法是供自己模塊內部調用的。

_.extend(View.prototype, Events, {
    // 這裏是各種對View.prototype的拓展,定義各種方法
});

還有一個比較大的共同點,就是slient參數。這個參數決定了是否要trigger一個事件,在源碼用佔了很大的篇幅對其進行分類討論。


3. Model

3.1 關鍵方法

有一些關鍵的方法一進入函數就會根據傳入的參數的形態進行變化。因為backbone一些方法支持兩個參數傳入或者一個數組傳入,這時候需要有個判斷。

3.2 set

set方法在model裏面是個很不好理解的東西,看了網上大多數解析感覺都很模糊(而且遇到難理解的就用一些藉口矇混過去)。不得不説set裏面複雜精妙程度是每讀一遍驚歎一遍。
我想以變量的角度來講解可能是一個比較好的角度。

  • changingthis._changing
    如果這個函數只是從頭執行到尾,那説實話,這兩個變量沒有任何意義。因為他們的值是確定的。看函數開頭:

var changing   = this._changing;
this._changing = true;

在函數結尾:

this._changing = false;

這個changing將永遠永遠是false。我上網看到有人説可能是webWorker,多線程相關的東西,但我直接在源碼console的時候卻發現,這個changing是會變的,而且我用得是todo範例。todo範例沒有任何類似webWorker的東西。這個假設猜測應該來説是不正確的。(不過這篇文章講得也很不錯啊)
所以這個changing到底有什麼用呢?答案就是遞歸函數set裏明明沒有遞歸啊?其實遞歸藏在了所有trigger的事件的回調函數裏面。源代碼下面的這一段:

// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
while (this._pending) {
    options = this._pending;
    this._pending = false;
    this.trigger('change', this, options);
}

這一個while裏的trigger使得函數發生遞歸,然後重新調用set。這樣的話,下一次changing就等於true了,這個變量的作用才能發揮。可以看一下這個鏈接裏面的講解。

  • current變量是用來作為引用改變attributes的,其實是set能設置attributes的本質。

  • changes數組是用來存放改變了的key的,用於後期的事件觸發。

  • changed & _previousAttributes
    把這兩個放到一起是因為他們的一個特殊的地方。我在todo的主函數的render裏面console,發現不論我做什麼操作,changed === {},_previousAttributes沒有發生改變。後來在查看官方文檔的時候,才瞭解previous的用法:

var bill = new Backbone.Model({
  name: "Bill Smith"
});

bill.on("change:name", function(model, name) {
  alert("Changed name from " + bill.previous("name") + " to " + name);
});

bill.set({name : "Bill Jones"});

set方法在被調用的時候,previous只有在回調函數裏才能有用,也就是説,在回調函數外面想要用這個previous獲取前一個值是不可能的。它只能獲取到當前值。為什麼呢?源碼做出瞭解釋。當用户做出操作需要用到set方法的時候,其實set方法並不是直接執行完就結束了。在這個方法裏面觸發了很多的事件,而previous只有在函數裏觸發了的事件的回調函數“裏面”才能返回正確的“前一個值”。changed也同理,因為不論中間如何變化,遞歸,到最後它會被設置為{}

3.3 save

save方法的作用是把當前model的狀態保存到數據庫中,因此不可避免地要用到ajax。由於backbone已經有了一個封裝好的方法sync用於觸發ajax,因此在save當中重點是設置參數。需要設置的有successerrormethod

  • success裏面會調用用户傳入的回調函數並觸發sync事件表示已經同步了。

  • error用封裝好的wrapError函數,這個函數用得很多,用於處理錯誤。

  • method根據實際要用那種方法設置
    其中比較值得注意的是wait參數。這個參數會影響頁面更新的時機。如果waittrue的話,就會需要等到服務器端相應才更新頁面,否則就會立即更新。

3.4 destory

destory方法也是與ajax有密切聯繫的。主要也是設置ajax參數。它分了幾種不同的情況並作出了相應的處理:

  • waitfalse,不用等待。發起delete請求,觸發內部函數destory

  • waittrue,發起ajax,等待服務器響應才觸發destory更新頁面。

  • 這是一個新的model,那就不需要發起請求了。

3.5 isValid

驗證函數,通過調用內部函數_validate,在通過這個函數調用validate函數。然後返回一個錯誤,如果沒有錯誤就返回true,否則觸發invalid,返回false


4. Collection

Collection類似一個數組,裏面存放着各種以model為結構的對象。在Collection中也有這形式的判斷,如果傳入的參數是單個對象就會被轉換成數組。

4.1 set

這是Collection的一個很常用的方法,源碼中這一段很長,也有點繁瑣,但是沒有特別難以理解的地方。整個set的結構是:

  • 設置幾個數組(下面會詳細講)

  • 設置實際的models(修改this.models

  • trigger事件

主要來説就是有如下幾個關鍵點:

  • 如果不符合model形式,轉換之。

  • 設置相應的插入位置at

  • 設置set數組。set數組在裏面作用是為給後面排序做準備。裏面存放的是新的Collectionmodels

  • 設置toAdd數組。這個數組是用於存儲新建的合法的model,然後需要調用內部函數_addReference設置索引於_byId數組,並且添加all事件(後面就可以通過model直接trigger事件)。當slient不是true,後期可以通過遍歷它來觸發add事件。

  • 設置toMerge數組。當這個model是原本已經存在的model的時候(cid匹配),就會修改,然後被push進這個數組中。

  • 設置toRemove數組。然後通過內部函數_removeModels刪除那些已經不在set裏面的models

  • 修改this.models,分兩種情況,一種是直接整個替換掉,一種是後面再添加。

  • 如果silent不是true就要觸發事件。特別值得注意的一點是:這裏面的事件有兩種,一種事件是由Model發出的,一種事件是有Collection發出的。從Model發出的事件可以很容易_addReference函數中發現

model.on('all', this._onModelEvent, this); 

在這裏註冊了,調用的是_onModelEvent函數。而其他沒有註冊的函數應該是給使用者註冊監聽用的。

4.2 sort

sort所依據的是用户傳入的comparator參數,這個參數可以是一個字符串也可以是一個函數,如果是字符串就通過underscoresortBy方法,如果是個函數就直接傳入sort的第二個參數中。

4.3 fetch & create

fetchcreatebackbone與服務器端交互的一個接口。兩個方法內部處理其實都很好理解,就是設置ajax參數。最終本質上都是觸發sync。但是唯一不同的是fetch是通過自身的sync函數,但create是通過調用modelsave,然後觸發sync的。在

model.save(null, options);

跟着這個save函數裏面走,就會發現參數null傳入是有意義的。在save裏面的參數設置會很好地賦值並最後觸發sync,而且有一個很有趣的點,就是這個createmodel傳上服務器,但是這個model是一個相對獨立的狀態,僅僅通過它的Collection屬性來維繫和Collection的關係。那就要求後端需要把這一個model添加到相應的Collection數據裏面去。

4.4 reference

Collection有一個值得關注的內部變量,那就是_byId,這個變量用cidid(所以model是一對一對出現的)來存儲Collection裏面的model,方便直接性的存取。在源碼中有很多操作目的就是刪除,增加,獲取這個內部變量的值。

4.5 CollectionIterator

這東西我覺得很有意思...在官方文檔裏面沒有提到,但是由於涉及到ES6的東西所以覺得有點眼前一亮的感覺(哈哈哈),backbone在這裏用了Symbol.iterator,具體用法在這個鏈接裏有介紹,還是挺清晰的。通過設置CollectionIteratorSymbol.iteratornext方法。它通過內部變量_kind來區分種類,_index來確定對應的next的結果,這個對於寫迭代器還是有點借鑑意義的~


5. View

在寫backbone應用的時候,View寫着寫着會越來越大...追根溯源,就是View的代碼很少...(大霧)。關於View,在寫相關代碼的時候有一些值是需要設置的(可選的)。下面的代碼就展示了可設置的參數,這些參數在View的方法中會用到(如果有的話)。

var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];

下面我會從兩個大的方面來解讀源碼,一個是element,一個是Events。整個View的源碼事實上就是這兩組東西。

5.1 Element

View字面意思是視圖,而在瀏覽器中,視圖就是html所呈現的頁面。每一個View事實上就對應着html的一個元素(當然這個元素裏面可以有很多很多元素)。這個元素默認標籤是div。與元素相關的代碼其實很簡單,首先要認清this.elthis.$el。前者是真正的節點,後者則是jquery對象的節點。後者由於是jquery式的,因此就可以做相關的jquery的操作。因此事件發起,刪除節點,設置屬性的操作都是jqueryapithis.$el或其子節點的操作。在進入構造函數的時候會調用一個叫_ensureElement的內部函數,在這個函數裏會根據用户設置的參數去構建節點,最後展現到頁面之上。

5.2 Events

事件是View中非常重要的組成。這是用户可以操作數據的一個接口。在View裏面和數據相關的方法有delegateEventsdelegatesundelegateEventsundelegate。裏面通過使用者設置的events屬性來創建各種事件,操作各種事件。

{
    'mousedown .title':  'edit',
    'click .button':     'save',
    'click .open':       function(e) { ... }
}

events相關代碼很簡單,但是有一個非常非常巧妙的地方:就是作者用了jquery事件相關api命名空間。在delegate被調用的時候就給事件加上了一個特定的命名空間。

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

因此在後續需要對整體的所有事件進行操作的時候就會方便很多很多。


6. 最後的話

這次源碼解析不能百分百保證是正確的,有一些混雜了自己的思考。因為不想像其他大部分的源碼解析那樣,對於問題模糊處理。但我覺得還是有意義的,因為每個人讀的角度不一樣。兼聽則明,也希望讀者能夠包容,希望深刻理解backbone的讀者也請多讀幾篇文章,多讀幾遍源碼。下一篇文章要寫router & history,這一個模塊可以單獨拆出來作為SPA的一個入口,個人認為這部分時backbonebackbone(骨架)。

希望能夠堅持更下去吧,開學了,事情也開始多了起來...

本人還是backbone小白,如果哪裏説錯了或者怎樣,請輕噴~相互學習~

下面是全部的文章:

  • 基於 Backbone + node 的個人簡歷生成器(個人學習總結)

  • Backbone源碼解讀(一)

  • Backbone源碼解讀(二)

  • Backbone源碼解讀(三)

user avatar lanlanjintianhenhappy 頭像 ivyzhang 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.