本文已同步在我的博客
在這個react和vue如日中天、jquery逐漸被大家拋棄的年代,我還是想要來說一說backbone。
16年6月初,在沒有任何前端框架使用經驗、js水平也較一般的情況下,被告知需要在幾個工作日內搭建完成一個后臺管理系統,沒有頁面設計稿、沒有組件庫,一切都是從零開始。當時面臨兩個選擇,backbone和react。雖然我很希望能夠拿react來練手,但是考慮到學習成本和項目時間問題,leader還是建議我使用backbone來完成,就這樣,一直用到差不多現在。雖然到項目后期業務場景越來越復雜,backbone的這套技術棧體現出越來越多的問題,但是對于小型項目來說,我還是認為backbone是個不錯的選擇,而且學習成本低,上手極快~
backbone是個非常輕量的mvc庫,本文將基于backbone的源碼談一談其實現的核心部分,以及其中一些巧妙的思想和代碼實現技巧。
事件機制(Events)
事件部分的核心邏輯其實比較簡單,簡化一下可以用如下的偽代碼來表示:
var events = {
evt1: handlers1,
evt2: handlers2,
...
}
//注冊事件
function on(name, callback) {
const handlers = events[name] || (events[name] = []);
handles.push(callback);
}
//觸發事件
function trigger(name) {
if (!events[name]) {
return;
}
const handlers = events[name];
for (let i = 0, len = handlers.length; i < len; i++) {
handlers[i]();
}
}
//解除綁定
function off(name) {
delete events[name];
}
當然了,以上寫法有很多細節的地方沒有加入進來,比如上下文綁定、對多種傳參方式的支持、觸發事件時對事件處理器傳參的處理等等。
我們知道,對于MVC來說,M(模型)的變化會反映在V(視圖)上,實際上是視圖監聽了模型的變化,再根據模型去更新自身的狀態,這當中最重要的一個功能就是監聽(listen)。該功能也是由Events部分實現的,包括:listenTo、stopListening等
listenTo和on類似,都是監聽一個事件,只不過listenTo是監聽其他對象的對應事件,而on是監聽自身的對應事件。stopListening同理。比如:
a.on('testevent', function(){
alert('1');
});
a.trigger('testevent');
如果其他對象希望監聽a的testevent事件呢?則可以通過listenTo來實現:
b.listenTo(a, 'testevent', function() {
alert('catch a\'s testevent');
})
其中第一個參數為要監聽的對象,第二個參數為事件名稱
當調用on方法的時候,會為對象自身創建一個_event屬性;而調用listenTo方法時,會為監聽對象創建_event屬性,同時為了記錄監聽者,被監聽對象還會創建一個_listeners屬性:
a.on('testevent', handlers1);
a會變成:
{
_events: {
testevent: [handlers1]
},
on: function() {
//...
}
...
}
當有其他對象監聽a時,如:
b.listenTo(a, 'testevent', handlers2);
a會變成:
{
_events: {
testevent: [handlers1, handlers2]
},
_listeners: b,
on: function() {
//...
}
...
}
在事件機制的實現部分,除了核心邏輯之外,在對一些方法的使用上,也很考究。為了綁定函數執行的上下文,我們經常會使用apply,call這些方法,而源碼中多次提到apply的執行效率要低一些,因此,有這樣的實現:
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
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;
}
};
有關為什么call比apply的效率更高的解釋可以參考這篇文章
模型(Model)
model用于維護數據,其中最關鍵的是對數據的更新部分,即set
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = options;
for (var i = 0; i < changes.length; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
if (!silent) {
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
每次set數據的時候,根據數據變化的部分,使用trigger方法觸發相應的事件;在set數據時,如果不希望觸發change事件,可以設置silent為true。
這部分比較容易讓人產生疑惑的是while循環部分,這個while循環有什么用呢?舉個例子:
new Model.on("change", function() {
console.log('model change');
}).set({
a: 1
});
以上代碼是最簡單的情況,監聽change事件,當model變化時,打印出model change
在源碼中,當第一次進入while后,緊接著this._pending被置為false,而事件觸發回調函數也不會更改this._pending的值,因此再次判斷時條件不成立,while內的代碼段只會執行一次。
但是實際情況往往不是這么簡單,如代碼注釋中所說,有可能會有嵌套的情況,比如:
new Model.on("change", function() {
this.set({
b: 1
})
}).set({
a: 1
});
在這種情況下,第一次trigger觸發change的回調函數中,又再次對model進行了更新操作,
this.set({
b: 1
})
每次set時,會更新this._pending為true,這樣當set b后,就會再次進入while內,觸發change事件。而如果沒有使用while循環的話,對b屬性更新的操作就無法觸發change事件,導致其監聽者到無法根據最新的數據更新自身狀態。
視圖(View)
View部分的實現比較簡單,其中最主要的是events部分,通常在一個View中,都會綁定一些dom事件,比如:
{
'click .preview-btn': 'preview',
'click .save-btn': 'save'
}
主要有兩點需要說明:
- backbone中是采用的事件委托的方式綁定事件,因此,一些不冒泡的事件,比如scroll,是無法通過這樣的方式綁定的
- 回調函數會保持正確的this指向。backbone內部進行了處理
delegateEvents: function(events) {
events || (events = _.result(this, 'events'));
if (!events) return this;
this.undelegateEvents();
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[method];
if (!method) continue;
var match = key.match(delegateEventSplitter);
this.delegate(match[1], match[2], _.bind(method, this));
}
return this;
}
結語
以上部分介紹了backbone中最核心部分的實現機制??梢钥吹狡鋵崿F非常的簡單,但是對于小型項目來說,確實可以幫我們做一些對數據的維護和管理工作,提高開發效率。但是隨著業務逐漸復雜,會越來越發現,backbone所能做的實現有限,而對于數據維護部分也非常不方便,尤其是需要是對多個模塊間的通信和數據維護問題。后續我會結合在復雜業務中的使用談一談backbone的缺點,以及更優的框架能帶來的便利。
說句題外話,雖然去年由于時間原因選擇了backbone,這一年基本沒有在復雜業務場景中使用react技術棧,都是自己做個小demo練手。但是也正是因為有了使用backbone去寫復雜業務的經歷,在數據維護上和模塊間通信上非常麻煩,以及backbone渲染dom時直接全部更新的會導致的頁面渲染性能問題,才更讓我感覺react + redux的美好。知其然,還需知其所以然啊~ ~
不然我覺得我可能會一直疑惑為什么要用一套這么復雜的技術棧,異步請求這塊寫起來還那么麻煩。這么看,壞事也算是好事了吧~~
查看原文
16年6月初,在沒有任何前端框架使用經驗、js水平也較一般的情況下,被告知需要在幾個工作日內搭建完成一個后臺管理系統,沒有頁面設計稿、沒有組件庫,一切都是從零開始。當時面臨兩個選擇,backbone和react。雖然我很希望能夠拿react來練手,但是考慮到學習成本和項...