Backbone Events源碼學(xué)習(xí)
寫在前面
backbone作為mvc框架在當(dāng)前前端開發(fā)中已經(jīng)有點(diǎn)過時(shí)了,個(gè)人感覺還是有點(diǎn)笨重,不夠輕巧吧。但由于其實(shí)很多項(xiàng)目還依賴backbone,另外其MVC框架的設(shè)計(jì)思想也值得借鑒,源碼2000行不到的長度,值得一讀。
Events用途
Backbone.Events在Backbone中承載著事件機(jī)制的角色,可以理解為一條事件總線,不同的元素可以通過觸發(fā)(自身/其他元素)事件、監(jiān)聽(自身/其他元素)事件來實(shí)現(xiàn)代碼的解耦(不必在一個(gè)元素的事件監(jiān)聽器,如jquery的click回調(diào),中處理其他元素的變化),不過這種代碼式的監(jiān)聽比起后來vuejs聲明式監(jiān)聽(watch、computed)還是要繁瑣和復(fù)雜不少。
除了提供了Backbone使用者監(jiān)聽、觸發(fā)事件的事件總線外,Backbone內(nèi)部Model、Collection也依賴事件總線進(jìn)行增刪查改等本地以及與服務(wù)器的數(shù)據(jù)交互。
Events在Backbone中的定位
事件總線,可以減輕不同元素之間的耦合度。
Events使用示例
<html>
<head>
</head>
<body>
<div class="a">
<span class="text">原始A文案</span>
<button class="btn">按鈕a(同時(shí)監(jiān)聽b按鈕)</button>
</div>
<br/>
<br/>
<div class="b">
<span class="text">原始B文案</span>
<button class="btn">按鈕b</button>
</div>
<br/>
<br/>
<div class="c">
<button class="btn">按鈕c(只監(jiān)聽一次的事件)</button>
</div>
<script type="text/javascript" src="underscore-min.js">
</script>
<script type="text/javascript" src="./jquery-3.1.1.min.js">
</script>
<script type="text/javascript" src="./backbone-min.js">
</script>
<script>
var textA = $(".a .text");
var textB = $(".b .text");
_.extend(textA, Backbone.Events);
_.extend(textB, Backbone.Events);
$(".a .btn").click(function(){
textA.trigger("click");
});
textA.on("click", function(){this.html("a按鈕被點(diǎn)擊")});
textB.listenTo(textA, "click", function(){$(".b .text").html("監(jiān)聽到a文案被修改");});
var listener = _.extend({}, Backbone.Events);
listener.once("click", function(){alert("自己被點(diǎn)擊");});
listener.listenToOnce(textA, "click", function(){alert("監(jiān)聽到a文案被修改");});
$(".c .btn").click(function(){
listener.trigger("click");
});
</script>
</body>
</html>
上面的例子分別實(shí)現(xiàn)了A文字區(qū)域監(jiān)聽A按鈕點(diǎn)擊事件,B文字區(qū)域監(jiān)聽A按鈕點(diǎn)擊事件和非dom對(duì)象監(jiān)聽一次按鈕事件。
Event的源碼實(shí)現(xiàn)
下面是理解Events實(shí)現(xiàn)的重頭戲,源碼剖析。
Events可供外部調(diào)用的api有如下幾個(gè):on/listenTo/off/stopListening/once/listenToOnce/trigger/bind/unbind 。(bind和unbind是on和off的alias)
on、off是監(jiān)聽/解除監(jiān)聽自身的事件,listenTo和stopListening是監(jiān)聽/解除監(jiān)聽其他對(duì)象的事件,像obj.trigger的調(diào)用能夠觸發(fā)obj的某個(gè)事件。
內(nèi)部api
Events底層通過iternalOn/onceMap/onApi/offApi/eventsApi實(shí)現(xiàn)。其中eventsApi是最為基礎(chǔ)的一個(gè)函數(shù),它負(fù)責(zé)遍歷傳入的事件(支持單個(gè)事件/空格分隔的多個(gè)事件/jquery風(fēng)格的map結(jié)構(gòu)的事件,如:{event:callback})
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;
};
eventsApi做的事情很簡單,將name拆分(如果有多個(gè)event事件名的話),然后對(duì)每個(gè)event調(diào)用參數(shù)里的iteratee方法。(傳入的iteratee是個(gè)方法名)
另外,如果采用jquery的風(fēng)格傳入map結(jié)構(gòu)的name,則要講opts的context設(shè)置為回調(diào)函數(shù)。(這個(gè)相當(dāng)于是callback執(zhí)行的this指針)
綁定一個(gè)對(duì)象的事件監(jiān)聽
下面我們來看如何實(shí)現(xiàn)監(jiān)聽自身的事件。
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
Events.on = function(name, callback, context) {
return internalOn(this, name, callback, context);
};
// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to
// for easier unbinding later.
Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = listeningTo[id];
// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
if (!listening) {
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
}
// Bind callbacks on obj, and keep track of them on listening.
internalOn(obj, name, callback, this, listening);
return this;
};
// Guard the `listening` argument from the public API.
var internalOn = function(obj, name, callback, context, listening) {
obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
context: context,
ctx: obj,
listening: listening
});
if (listening) {
var listeners = obj._listeners || (obj._listeners = {});
listeners[listening.id] = listening;
}
return obj;
};
on其實(shí)直接做了一層proxy轉(zhuǎn)發(fā)到了interalOn函數(shù)內(nèi),然后通過onApi的調(diào)用來完成對(duì)事件的監(jiān)聽。
// The reducing API that adds a callback to the `events` object.
var onApi = function(events, name, callback, options) {
if (callback) {
var handlers = events[name] || (events[name] = []);
var context = options.context, ctx = options.ctx, listening = options.listening;
if (listening) listening.count++;
handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
}
return events;
};
onApi的流程則是首先判斷回調(diào)函數(shù)是否為空,非空才做處理。
每個(gè)對(duì)象都有一個(gè)_events 屬性來記錄自己監(jiān)聽了哪些事件。(是一個(gè)鍵值對(duì)屬性,鍵為事件名,值為一個(gè)列表),列表里的每個(gè)元素表示一個(gè)處理器,包含了回調(diào)函數(shù)、context、ctx、listening幾個(gè)屬性。其中l(wèi)istening表示誰在監(jiān)聽這個(gè)事件,也就是下一節(jié)的內(nèi)容。
總結(jié):實(shí)際上監(jiān)聽事件的過程就是將封裝好的callback信息添加到對(duì)象_events屬性對(duì)應(yīng)事件名的隊(duì)列中的過程。
對(duì)象A對(duì)對(duì)象B的事件監(jiān)聽
下面我們看下對(duì)其他對(duì)象事件的監(jiān)聽實(shí)現(xiàn)。
// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to
// for easier unbinding later.
Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = listeningTo[id];
// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
if (!listening) {
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
}
// Bind callbacks on obj, and keep track of them on listening.
internalOn(obj, name, callback, this, listening);
return this;
};
首先會(huì)判斷一下當(dāng)前自己是否已經(jīng)監(jiān)聽了B對(duì)象。如果沒有,則封裝B對(duì)象的信息并添加到listeningTo隊(duì)列中。
然后直接調(diào)用剛剛的interalOn函數(shù),與之前不同的是,需要傳入listening對(duì)象,并將context改為this。這樣當(dāng)B對(duì)象相應(yīng)事件發(fā)生的時(shí)候就會(huì)調(diào)用callback,并且this指針會(huì)指向A對(duì)象。(真正生效的this其實(shí)是一個(gè)ctx的內(nèi)部屬性,它的值為context||obj, 即以傳入的優(yōu)先,如果沒有傳入則是對(duì)象本身)
一次性的監(jiān)聽事件
還有一類事件比較特殊,就是回調(diào)一次就不再監(jiān)聽的事件。
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, its listener will be removed. If multiple events
// are passed in using the space-separated syntax, the handler will fire
// once for each event, not once for a combination of all events.
Events.once = function(name, callback, context) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
if (typeof name === 'string' && context == null) callback = void 0;
return this.on(events, callback, context);
};
// Inversion-of-control versions of `once`.
Events.listenToOnce = function(obj, name, callback) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
return this.listenTo(obj, events);
};
// Reduces the event callbacks into a map of `{event: onceWrapper}`.
// `offer` unbinds the `onceWrapper` after it has been called.
var onceMap = function(map, name, callback, offer) {
if (callback) {
var once = map[name] = _.once(function() {
offer(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
}
return map;
};
通過onceMap生成一個(gè)jquery風(fēng)格的map,其實(shí)是對(duì)我們傳入的callback進(jìn)行了一層裝飾。在事件回調(diào)的過程中,首先解除監(jiān)聽,然后繼續(xù)原有的業(yè)務(wù)邏輯。
把調(diào)用一次和解除的邏輯通過裝飾模式結(jié)合在一起,省去了業(yè)務(wù)對(duì)特定邏輯的開發(fā)。
事件觸發(fā)回調(diào)機(jī)制
每個(gè)Events對(duì)象內(nèi)部有一個(gè)_events對(duì)象,用于保存當(dāng)前對(duì)象監(jiān)聽的事件。當(dāng)外部通過trigger觸發(fā)事件時(shí),內(nèi)部實(shí)現(xiàn)如下:
// Handles triggering the appropriate event callbacks.
var triggerApi = function(objEvents, name, callback, args) {
if (objEvents) {
var events = objEvents[name];
var allEvents = objEvents.all;
if (events && allEvents) allEvents = allEvents.slice();
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, [name].concat(args));
}
return objEvents;
};
// 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;
}
};
由于Events提供了對(duì)象監(jiān)聽所有事件的功能,如果obj.on('all', function(){}) 這種形式可以處理對(duì)象的所有事件,另外事件回調(diào)的參數(shù)會(huì)得到事件名。
很多Backbone內(nèi)部的trigger事件都帶三個(gè)參數(shù),這里Events也提供了事件回調(diào)接收多個(gè)參數(shù)的能力。
解除監(jiān)聽
最后講講如何解除監(jiān)聽,實(shí)際上我覺得這也是Events最難懂的一部分。
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
Events.off = function(name, callback, context) {
if (!this._events) return this;
this._events = eventsApi(offApi, this._events, name, callback, {
context: context,
listeners: this._listeners
});
return this;
};
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
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;
};
// The reducing API that removes a callback from the `events` object.
var offApi = function(events, name, callback, options) {
if (!events) return;
var i = 0, listening;
var context = options.context, listeners = options.listeners;
// Delete all events listeners and "drop" events.
if (!name && !callback && !context) {
var ids = _.keys(listeners);
for (; i < ids.length; i++) {
listening = listeners[ids[i]];
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
}
return;
}
var names = name ? [name] : _.keys(events);
for (; i < names.length; i++) {
name = names[i];
var handlers = events[name];
// Bail out if there are no events stored.
if (!handlers) break;
// Replace events if there are any remaining. Otherwise, clean up.
var remaining = [];
for (var j = 0; j < handlers.length; j++) {
var handler = handlers[j];
if (
callback && callback !== handler.callback &&
callback !== handler.callback._callback ||
context && context !== handler.context
) {
remaining.push(handler);
} else {
listening = handler.listening;
if (listening && --listening.count === 0) {
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
}
}
}
// Update tail event if the list has any events. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}
return events;
};
解除監(jiān)聽最終都由offApi實(shí)現(xiàn)。如果沒有傳遞任何參數(shù),則會(huì)解除該對(duì)象所有事件的監(jiān)聽。
如果傳遞了,則在_events屬性中取出相關(guān)的監(jiān)聽器隊(duì)列,然后比較callback函數(shù)跟傳入的callback函數(shù)(這里針對(duì)只監(jiān)聽一次的once監(jiān)聽器還延伸了一個(gè)_callback屬性的概念),如果不相等則將監(jiān)聽器放入remain隊(duì)列。否則則刪掉相應(yīng)的監(jiān)聽。