Backbone源码解读
Backbone在流行的前端框架中是最轻量级的一个,全部代码实现一共只有1831行1。从前端的入门再到Titanium,我虽然几次和Backbone打交道但是却对它的结构知之甚少,也促成了我想读它的代码的原始动力。这个系列的文章主要目的是分享Backbone框架中可以用于日常开发的实践参考,力求能够简明扼要的展现Backbone Modal, Controller和Sync这些核心内容,希望能够对大家学习和使用Backbone有一些帮助。
这个系列的文章将包括以下3篇内容:
- Backbone源码解读之Events实现
- Backbone源码解读之Router, History实现
- Backbone源码解读之Model, Collection, Sync实现
本文是它的第一篇《Backbone源码解读之Events实现》
Backbone Events实现
Backbone的Event是整个框架运转的齿轮。它的优美之处在于它是Backbone的一个基础方法,通过_.extend的方法Mixin到Backbone的每一个模块中。
//Model
_.extend(Model.prototype, Events, {
changed: null,
//other Model prototype methods
//...
}
Event的基础概念
事件的管理是一个绑定在Events这个命名空间中的_events对象来实现的。
它的结构是name: [callback functions]的key-callback_array键值对。
this._events = {
change: [callback_on_change1, callback_on_change2, ....],
....
}
当事件发生的时候,event从这个对象中根据事件的名称取得回调函数数组,然后循环执行每个回调函数,也就说明了为什么多次绑定会重复触发多次事件。
Event包括on, off, trigger三个基础方法,其余的所有方法均是对它们的扩展。
on(name, callback, context)
on接受3个参数,包括事件的名称,回调函数和回调函数执行的上下文环境。其中context是可选参数,如果你不是很熟悉JS的执行上下文环境可以暂时不用管它。
抛开所有Backbone的花哨的检查,执行on的操作本质就是向_events中name对应的回调函数数组[callback functions]中Push新的函数。
简单来说代码实现就是这个样子:
Events.on = function(name, callback, context) {
if (callback) {
var handlers = events[name] || (events[name] = []);
handlers.push({callback: callback, context: context, ctx: context || this});
}
return this;
};
至于你在看源代码的时候会长很多,那是因为一方面Backbone要处理关于_events以及_events[name]未初始化的两种特殊情况。另一方面eventsApi,onApi这些方法是为了处理on时候你传入的不是一个string类型的名称和一个callback函数所做的条件处理。
例如下面两种方法都是合法的:
//传入一个名称,回调函数的对象
model.on({
"change": on_change_callback,
"remove": on_remove_callback
});
//使用空格分割的多个事件名称绑定到同一个回调函数上
model.on("change remove", common_callback);
但是核心其实都是同一个绑定函数。
值得注意的一点是由于Backbone接受all作为name的参数,并且将回调函数保存在_events.all中,关于它的执行详细可以参考trigger。
off(name, callback, context)
与on不同,off的3个参数都是可选的。
-
如果没有任何参数的时候,off相当于把对应的_events对象整体清空。
if (!name && !callback && !context) { this._events = void 0; return this; }
-
如果有name参数但是没有具体清除哪个callback的时候,则把_events[name]对应的内容全部清空。
if (!callback && !context) { delete this._events[name]; continue; }
-
如果还有进一步详细的callback和context的情况下,则进入[callback functions]中进行检查,移除具体一个回调函数的条件非常严苛,必须要求上下文和函数与原来完全一致,也就是说如果传入的callback或者context不是原有on对象的引用,而是复制的话,这里的off是无效的。
var remaining = []; if( callback && callback !== handler.callback && callback !== handler.callback._callback || context && context !== handler.context ){ //保留回调函数在数组中 }
trigger(name)
trigger取出name对应的_events[name]以及_event.all中的callback函数。
需要注意的一点是,触发对应名称的callback和all的callback使用了不一样的参数,all的参数中还包含了当前事件的名称。
//当绑定3个以下回调函数的时候Backbone会做如下优化处理,据说这样是可以提高执行效率的。
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事件使用
从使用上来讲,对一个对象进行事件的on绑定。然后在同一个对象的其他函数执行过程中,或者其他的对象中,触发该对象的trigger方法和对应的事件名称来执行其上绑定的callback函数。
其他辅助函数
接下来再看看Event中进一步定义了哪些其他辅助函数
once(name, callback, context)
效果相同与on,不过对应的callback函数仅执行一次。当然,你同样可以在once绑定的回调函数执行前手动通过off将其移除。
来看看Once的实现,由于添加了执行过程中的移除方法,once在实际实行on的时候使用了如下匿名函数:
var once = _.once(function() {function(){
self.off(name, once);
callback.apply(this, arguments);
});
return this.on(name, once, context);
但是细心的你一定发现,保存在_event数组中的函数是once这个匿名函数了。但是用户并不知道Backbone的这些操作,在取消绑定时仍然会使用原来的回调函数来试图解除绑定。上面我们也提到,必须使用完全一致的函数才能够取消绑定,那么为什么还能够成功呢?
这里Backbone做了一个小小的操作,不知道你有没有注意到上面off函数中有这样一行内容?
callback !== handler.callback._callback
既然callback是我们传入的回调函数,那么哪里来的_callback这个属性呢?答案就在once里面。
var once = _.once(function() {...取消绑定,执行callback);
once._callback = callback;
也就是Backbone在返回之前悄悄的为once这个函数添加了一个_callback的属性,用来保存原来的回调函数,这样用户在传入原来的回调函数取消绑定的时候,off会检查函数时候有_callback这个属性和用户传入的函数匹配,同样可以取消绑定。
listenTo(obj, name, callback)、listenToOnce(obj, name, callback)和stopListening(obj, name, callback)
除了将对象本身expose给另一个对象,让另一个对象执行trigger方法触发该对象上绑定的event以外。Event还进一步提供了listenTo系列的方法,执行逻辑正好与on相反。
例如有如下要求,当B对象上发生事件b的时候,触发A对象的callbackOnBEvent函数。
// 使用on的情况下
B.on(“b”, A.callbackOnBEvent)
// 使用listenTo的情况下
A.listenTo(B, “b”, callbackOnEvent);
从实现上看,它门的区别就在于谁负责管理这个事件。第一个模型中,B就像是整个系统的master,负责事件到达的时候的分发,让不同的对象(如A)执行对应的方法。第二个模型中,B更像是一个信息栈,A监听B上发生的事件,并且在对应事件到达的时候触发自身相应的回调函数。两者并无好坏之分,但是从系统架构上来说因为本身回调函数的上下文环境就是A,所以listenTo的方式可能会来的更加自然,而且由A自己来控制什么时候移除回调的执行,也可以让代码的解耦程度更高。
超越Backbone
使用Event方法来处理异步请求让代码的可读性大大增加。如果你的单页面应用恰好使用了Backbone作为前端框架,将Event通过Backbone.Events这个变量暴露出来,你可以使用类似Model扩展的方法
//Your object
_.extend(your_object.prototype, Backbone.Events, {
//other prototype methods
//...
}
这样你的Object也就具有了彼此绑定事件、触发事件的能力。
即便你的前端并没有使用Backbone,由于Events并不依赖Backbone的其他部分实现,你完全可以将它放到自己的代码lib中,作为一个基础方法来使用。
类似的方式你也可以经常在Node的后端看到
var util = require("util");
var events = require("events");
function MyStream() {
events.EventEmitter.call(this);
}
util.inherits(MyStream, events.EventEmitter);
总之,我个人是非常推荐多多使用Event来替代层级的Callback结构。
-
根据2015年4月 稳定版本Backbone.js 1.1.2的注释版本。Master上的代码和注释版本稍有出入,哪位大神知道为什么吗?? ↩
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。