本文同步更新於www.devsai.comjavascript
一直想着讀讀源碼,但一直沒有找到目標,一些流行的框架,大多代碼量很多。java
就像是面對着高聳如雲的山峯,擡頭望去,就已經沒了攀登的勇氣。git
俗話說的好,凡事得一步一個腳印,一口吃不出個胖子。github
大框架搞不定,能夠短小精悍的類庫下手。api
打BOSS前一定要殺掉無數的小怪。數組
而,backbone就是個很是好的選擇,加上它的註釋也就2000行左右。app
也在網上看到一些對Backbone源碼的解析,但或多或少的有如下幾個狀況:框架
最後一點,也是最重要的一點,並非閱讀者的想法不對,
而是想,若是本身去閱讀,或許能獲得不一樣的想法。jsp
並且對於閱讀源碼的來講,他從源碼中得到的收穫,必定是要比寫出來的多。函數
我建議你們去看別人對一些源碼的解析,更建議本身也去試着讀讀源碼。
這樣,本身對源碼更深刻理解的同時,還能夠對別人作的分析,進行更深層次的探討。
本文中會出現部分的源碼,點擊這裏查看完整源碼
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
和bind
是徹底同樣的,只是取了個別名。方便你們的使用習慣。
listenTo
官方說明是對on
控制反轉。如何反轉,後面具體說明。
once
就很好理解了,註冊的事件只執行一次,完了自動解綁。這也就是爲何下面的解綁方法中沒有對其解綁的動做了。(一次性筷子,用完就扔,不須要洗)
一樣的off
與unbind
除了方法名不一樣外,做用徹底同樣。
stopListening
也是用來解綁的,但它比較厲害了,對調用對象解綁解的不折不扣。
經過此方法能夠觸發單個或同時觸發多個事件。trigger(eventname)
, 第一個參數爲事件名,其餘的參數爲傳給事件執行函數的參數。
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
綁定後的解綁操做。
先舉個例子,執行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將多一個。
在不少的類庫中使用的事件機制都是沒有這兩個方法的功能。
這兩個方法更像是專爲view,model而生的。
經過這兩個方法能夠方便的對view相關的對象監聽事件進行跟蹤,解綁。
_events
如上的model._events
,咱們來分析下它裏面有些什麼:
model._events
它是一個對象 : { key1 : value1, key2 : value2 , key3 : value3 ....}
。以事件名爲key, value則是一組組數,數組內的每一元素又是一個對象
元素中的對象內容以下:
on
時,爲context參數,當調用view.listenTo(....)
時,爲調用的對象如:view。)context
與ctx
如上所述,每一個元素裏的 context
與ctx
幾乎同樣,那爲何須要兩個屬性呢。
經過閱讀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
主要是幹什麼的呢。
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 }
off
時不須要),context爲回調函數的上下文 , listening ,調用listenTo
時存在。once
,listenToOnce
, 調用這兩個接口時 opts
是個函數(作解綁操做)trigger
, 此時opts
是個數組(args,爲觸發事件傳時回調函數的參數)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源碼解析文章,請點下關注,第一時間得到更多更新內容。