本文同步更新於www.devsai.com
一直想著讀讀原始碼,但一直沒有找到目標,一些流行的框架,大多程式碼量不少。
就像是面對著高聳如雲的山峰,抬頭望去,就已經沒了攀登的勇氣。
俗話說的好,凡事得一步一個腳印,一口吃不出個胖子。
大框架搞不定,可以短小精悍的類庫下手。
打BOSS前必定要殺掉無數的小怪。
而,backbone就是個非常好的選擇,加上它的註釋也就2000行左右。
也在網上看到一些對Backbone原始碼的解析,但或多或少的有以下幾個情況:
- 一些Backbone解析,只做了部分就停更了
- Backbone解析的,據現在已有年代,解析的原始碼與現在的有略微的出入
- 對原始碼的解析,多少帶有閱讀者的想法
最後一點,也是最重要的一點,並不是閱讀者的想法不對,
而是想,如果自己去閱讀,或許能得到不同的想法。
而且對於閱讀原始碼的來說,他從原始碼中獲得的收穫,一定是要比寫出來的多。
我建議大家去看別人對一些原始碼的解析,更建議自己也去試著讀讀原始碼。
這樣,自己對原始碼更深入理解的同時,還可以對別人做的分析,進行更深層次的探討。
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
首先,on
和bind
是完全一樣的,只是取了個別名。方便大家的使用習慣。
listenTo
官方說明是對on
控制反轉。如何反轉,後面具體說明。
once
就很好理解了,註冊的事件只執行一次,完了自動解綁。這也就是為什麼下面的解綁方法中沒有對其解綁的動作了。(一次性筷子,用完就扔,不需要洗)
- 解綁事件 off,stopListening,unbind
同樣的off
與unbind
除了方法名不同外,作用完全一樣。
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會有listenTo和stopListening
在很多的類庫中使用的事件機制都是沒有這兩個方法的功能。
這兩個方法更像是專為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']
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
是內部的函式,所有對外的介面,都會直接或間接的呼叫它。複用率極高。
那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原始碼解析文章,請點下關注,第一時間獲得更多更新內容。