Backbone系列篇之Backbone.Events源碼解析

本文同步更新於www.devsai.comjavascript

一直想着讀讀源碼,但一直沒有找到目標,一些流行的框架,大多代碼量很多。java

就像是面對着高聳如雲的山峯,擡頭望去,就已經沒了攀登的勇氣。git

俗話說的好,凡事得一步一個腳印,一口吃不出個胖子。github

大框架搞不定,能夠短小精悍的類庫下手。api

打BOSS前一定要殺掉無數的小怪。數組

而,backbone就是個很是好的選擇,加上它的註釋也就2000行左右。app

也在網上看到一些對Backbone源碼的解析,但或多或少的有如下幾個狀況:框架

  • 一些Backbone解析,只作了部分就停更了
  • Backbone解析的,據如今已有年代,解析的源碼與如今的有略微的出入
  • 對源碼的解析,多少帶有閱讀者的想法

最後一點,也是最重要的一點,並非閱讀者的想法不對,
而是想,若是本身去閱讀,或許能獲得不一樣的想法。jsp

並且對於閱讀源碼的來講,他從源碼中得到的收穫,必定是要比寫出來的多。函數

我建議你們去看別人對一些源碼的解析,更建議本身也去試着讀讀源碼。
這樣,本身對源碼更深刻理解的同時,還能夠對別人作的分析,進行更深層次的探討。

Backbone.Events 事件機制

本文中會出現部分的源碼,點擊這裏查看完整源碼

Events 相關代碼有200多行

對外定義的方法有:

代碼開始,就先定義了Backbone.Events,這是爲何呢

由於Backbone的其餘部分對象都是繼承了Events,也是就說,Backbone.Model,Backbone.Collection,Backbone.View,Backbone.Router

均可以使用Events的屬性。

Backbone.Events也可使用在任何的對象上,就像這樣:var o=_.extend({},Backbone.Events);

而後o對象,就能夠爲所欲爲的作到訂閱/發佈了。

上述的API方法能夠分三部分:

  • 綁定事件 on,listenTo,once,bind

首先,onbind是徹底同樣的,只是取了個別名。方便你們的使用習慣。

listenTo官方說明是對on控制反轉。如何反轉,後面具體說明。

once就很好理解了,註冊的事件只執行一次,完了自動解綁。這也就是爲何下面的解綁方法中沒有對其解綁的動做了。(一次性筷子,用完就扔,不須要洗)

  • 解綁事件 off,stopListening,unbind

一樣的offunbind除了方法名不一樣外,做用徹底同樣。

stopListening也是用來解綁的,但它比較厲害了,對調用對象解綁解的不折不扣。

  • 觸發事件 trigger

經過此方法能夠觸發單個或同時觸發多個事件。trigger(eventname), 第一個參數爲事件名,其餘的參數爲傳給事件執行函數的參數。

listenTo(on的控制反轉)

object.listenTo(other, event, callback)複製代碼

讓 object 監聽 另外一個(other)對象上的一個特定事件。不使用other.on(event, callback, object),而使用這種形式的優勢是:listenTo容許 object來跟蹤這個特定事件,
而且之後能夠一次性所有移除它們。callback老是在object上下文環境中被調用。

這裏有個概念叫Inversion of Control(IoC控制反轉)
這是種主從關係的轉變,一種是A直接控制B,另外一種用控制器(listenTo方法)間接的讓A控制B。

經過listenTo把本來other主導綁定監聽事件,變成了由object主導綁定監聽事件了。

on比較

從功能上來講,on,listenTo是同樣的。

來看個例子:

var changeHandler = function(){}

model.on('change:name',changeHandler,view);複製代碼

或者能夠這樣

view.listenTo(model,'change:name',changeHandler);複製代碼

兩種方式的做用是同樣的,當model的name發生改變時,調用view中的方法。

可當view中不止有一個model時呢

功能上來說,仍是無差異,但若是想要當離開頁面時view須要銷燬,view中model綁定的事件也須要註銷時,看看兩種綁定方式,對面這問題時會怎麼辦

on的解綁

var view = {
    changeName :function(name){
       //doing something
    }
}
model.on('change:name',view.changeName,view);
model2.on('change:name',view.changeName,view);

//view離開時,model如何解綁
model.off('change:name',view.changeName,view);
model2.off('change:name',view.changeName,view);複製代碼

有多個model的話,須要進行屢次的解綁操做。

再來看看listenTo的解綁

view.listenTo(model,'change:name',view.changeName);
view.listenTo(model2,'change:name',view.changeName);

//解綁
view.stopListening();複製代碼

並不須要作更多的操做就能把view相關的監聽事件給解綁。

而經過查看stopListening

Events.stopListening = function(obj, name, callback) {
    var listeningTo = this._listeningTo;
    if (!listeningTo) return this;

    var ids = obj ? [obj._listenId] : _.keys(listeningTo);

    for (var i = 0; i < ids.length; i++) {
      var listening = listeningTo[ids[i]];

      // If listening doesn't exist, this object is not currently
      // listening to obj. Break out early.
      if (!listening) break;

      listening.obj.off(name, callback, this);
    }

    return this;
  };複製代碼

內部執行了屢次的.off(name, callback, this),至關於內部給作了用on綁定後的解綁操做。

深刻了解listenTo

先舉個例子,執行view.listenTo(model,'change',changeHandler), 執行過程看下面註釋:

Events.listenTo = function(obj, name, callback) {
    // obj = model
    if (!obj) return this;    

    // obj._listenId 不存在,執行 id = (obj._listenId = _.uniqueId('l')) == 'l1'
    var id = obj._listenId || (obj._listenId = _.uniqueId('l'));  

    // this._listeningTo 不存在,執行 listeningTo = (this._listeningTo = {})
    var listeningTo = this._listeningTo || (this._listeningTo = {});

    // listening = this._listeningTo[obj._listenId] : undefined == ({})['l1']
    var listening = listeningTo[id];

    // true 執行條件語句
    if (!listening) {
      // this._listenId == undefined , thisid = (this._listenId = _.uniqueId('l')) == 'l2'
      var thisId = this._listenId || (this._listenId = _.uniqueId('l'));

      // this._listeningTo[obj._listenId] = {....}
      listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
    }

    internalOn(obj, name, callback, this, listening);
    return this;
  };複製代碼

上述代碼執行中,會調用內部函數onApi(在internalOn內調用),執行handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});

執行完後:

model._listenId = 'l1'
view._listenId = 'l2'
view._listeningTo = {'l1' : {obj:model,objId : 'l1',id : 'l2',listeningTo: view._listeningTo,count : 0}}
model._listeners = {'l2' : view._listeningTo['l1'] }
model._event = {'change':[{callback: changeHandler, context: view, ctx: view, listening: view._listeningTo['l1']}]}複製代碼

view._listeningTo 的key 爲model._listenId , 也就是說,增長一個model實例,就會增長一個key,
例如再執行:view.listenTo(model2,'change',changeHandler)

因此經過_listeningTo屬性,可以知道view與多少個model有關聯。

這樣,當執行view.stopListening()時,就能把model,model2上的監聽事件所有移除了。

一樣的,
model._listeners的key 爲view._listenId, 例如:view2.listenTo(model,'change',changeHandler),
那麼會再生成一個view2._listenId, model._listeners的key將多一個。

爲何Backbone.Events會有listenTostopListening

在不少的類庫中使用的事件機制都是沒有這兩個方法的功能。

這兩個方法更像是專爲view,model而生的。
經過這兩個方法能夠方便的對view相關的對象監聽事件進行跟蹤,解綁。

事件對象上的_events

如上的model._events,咱們來分析下它裏面有些什麼:

model._events它是一個對象 : { key1 : value1, key2 : value2 , key3 : value3 ....}。以事件名爲key, value則是一組組數,數組內的每一元素又是一個對象

元素中的對象內容以下:

  • callback 事件的回調函數
  • context 回調函數的上下文對象(即當調用on時,爲context參數,當調用view.listenTo(....)時,爲調用的對象如:view。)
  • ctx 爲context ,當context不存在時,爲被監聽的對象,如:model.on(...)或view.on(model,...)中的model
  • listening 其實就是view._listeningTo中的某個屬性值,能夠當作: listening == view._listeningTo['l1']

contextctx

如上所述,每一個元素裏的 contextctx幾乎同樣,那爲何須要兩個屬性呢。

經過閱讀off方法及trigger方法就會知道,上面兩屬性在這兩個方法中分別被使用了。

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

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

好比下面的代碼:

var model = {  name : 'devsai'  }
var changeHandler = function(){ console.log(this.name)}
_.extend(model,Backbone.Events)
model.on('change',changeHandler)
model.trigger('change');  // print : devsai

model.off();
var context = { name : 'SAI'}
model.on('change',changeHandler,context)
model.trigger('change');  // print : SAI

model.off()
var view = { name : 'SAI listenTo' }
_.extend(view,Backbone.Events)
view.listenTo(model,'change',changeHandler)
model.trigger('change')   // print : SAI listenTo複製代碼

在調用trigger時,可能會執行這部分代碼

(ev = events[i]).callback.call(ev.ctx)複製代碼

但這邊,這種寫法我是有疑惑的,就如 ev.ctx在沒有context的狀況下, ctx 纔是obj(即被監聽的對象),
爲什麼不去掉ctx屬性, 而後在trigger時,作context判斷

例如把代碼改爲:

(ev = events[i]).callback.call(ev.context || ev.obj)複製代碼

這樣ctx屬性就能夠不去定義了。理解起來更直觀。

內部函數 eventsApi

eventsApi是內部的函數,全部對外的接口,都會直接或間接的調用它。複用率極高。

eventsApi主要是幹什麼的呢。

var eventsApi = function(iteratee, events, name, callback, opts) {
    var i = 0, names;
    if (name && typeof name === 'object') {
      // Handle event maps.
      if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
      for (names = _.keys(name); i < names.length ; i++) {
        events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
      }
    } else if (name && eventSplitter.test(name)) {
      // Handle space-separated event names by delegating them individually.
      for (names = name.split(eventSplitter); i < names.length; i++) {
        events = iteratee(events, names[i], callback, opts);
      }
    } else {
      // Finally, standard events.
      events = iteratee(events, name, callback, opts);
    }
    return events;
  }複製代碼

經過調用對外方法(如on,listenTo,once...)傳入的是'change update',callback{'change':callback,'change update':callback},而最終指向的內部API函數爲單個事件:eventName,callback

因此簡單說,該方法對多事件進行解析拆分,遍歷執行單個'eventname',callback

下面來具體說說eventsApi的參數

iteratee

是個函數,根據調用的對外接口不一樣,該函數也不一樣。

如:作綁定iteratee = onApi , onceMap; 作解綁 iteratee = offApi; 作觸發 iteratee = triggerApi


events

已有事件的集合,當前事件對象上綁定的全部事件


name

事件名,來源於各對外接口傳入的name

有兩種類型,string (例如:"change","change update"),map object (例如:{"change":function(){}, "update change":function(){}})


callback

回調函數,來源於各對外接口傳入的callback,但它也不必定老是回調函數,當name爲object時,callbcak多是context。


opts

根據調用的接口不一樣,有如下幾種狀況

  • on ,listenTo,off ,調用這三個接口時 opts是個對象,
    存放着{context: context,ctx: obj,listening: listening }
    obj爲被監聽的對象(off時不須要),context爲回調函數的上下文 , listening ,調用listenTo時存在。
  • once,listenToOnce , 調用這兩個接口時 opts是個函數(作解綁操做)
  • trigger , 此時opts是個數組(args,爲觸發事件傳時回調函數的參數)

內部函數 triggerEvents

var triggerEvents = function(events, args) {
    var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
    switch (args.length) {
      case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
      case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
      case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
      case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
      default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
    }
  };複製代碼

爲何要這麼寫呢,根據它的函數註釋的意思是說,在Backbone內部大部分的事件最多隻有3個參數,對事件調用進行了優化,
先嚐試使用call調用,儘可能的不去使用apply調用,以此達到優化的目的。

這裏有對call,apply性能對比測試 jsperf.com/call-apply-…

最後

歡迎你們來一塊兒探討backbone,因爲我的能力有限,若有描述不妥或不對之處,請及時聯繫我或評論我。

若是喜歡這篇文章,幫忙點個贊支持下。

若是但願看到後續其餘Backbone源碼解析文章,請點下關注,第一時間得到更多更新內容。

相關文章
相關標籤/搜索