本文同步更新于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源码解析文章,请点下关注,第一时间得到更多更新内容。