Backbone系列篇之Backbone.Events原始碼解析

devsai發表於2016-12-10

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

一直想著讀讀原始碼,但一直沒有找到目標,一些流行的框架,大多程式碼量不少。

就像是面對著高聳如雲的山峰,抬頭望去,就已經沒了攀登的勇氣。

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

大框架搞不定,可以短小精悍的類庫下手。

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

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

也在網上看到一些對Backbone原始碼的解析,但或多或少的有以下幾個情況:

  • 一些Backbone解析,只做了部分就停更了
  • Backbone解析的,據現在已有年代,解析的原始碼與現在的有略微的出入
  • 對原始碼的解析,多少帶有閱讀者的想法

最後一點,也是最重要的一點,並不是閱讀者的想法不對,
而是想,如果自己去閱讀,或許能得到不同的想法。

而且對於閱讀原始碼的來說,他從原始碼中獲得的收穫,一定是要比寫出來的多。

我建議大家去看別人對一些原始碼的解析,更建議自己也去試著讀讀原始碼。
這樣,自己對原始碼更深入理解的同時,還可以對別人做的分析,進行更深層次的探討。

Backbone.Events 事件機制

本文中會出現部分的原始碼,點選這裡檢視完整原始碼

Events 相關程式碼有200多行

對外定義的方法有:

Backbone系列篇之Backbone.Events原始碼解析

程式碼開始,就先定義了Backbone.Events,這是為什麼呢

因為Backbone的其他部分物件都是繼承了Events,也是就說,Backbone.Model,Backbone.Collection,Backbone.View,Backbone.Router

都可以使用Events的屬性。

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

然後o物件,就可以隨心所欲的做到訂閱/釋出了。

上述的API方法可以分三部分:

Backbone系列篇之Backbone.Events原始碼解析

  • 繫結事件 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原始碼解析文章,請點下關注,第一時間獲得更多更新內容。

相關文章