Backbone源码解读(一)事件模块
Backbone源码浅读:
前言:
Backbone是早起的js前端MV*框架之一,是一个依赖于underscore和jquery的轻量级框架,虽然underscore中基于字符串拼接的模板引擎相比如今基于dom元素双向绑定的模板引擎已显得落伍,但backbone作为引领前端mv*开发模式的先驱之一,依然是麻雀虽小却设计精妙,了解其设计与结构对于想一探mv*框架的初学者来说仍会获益匪浅。
Backbone结构:
Backbone分为几个部分:其中最核心的是Event事件模块,提供了实现事件与观察者模式的基础;随后是Model与Collection,提供了数据(Model)层面的抽象;接着是View,提供了数据与表现的相互链接,其模板引擎依赖于underscore的template方法;随后的async模块一直是我对Backbone又爱又恨的地方,一方面在代码层面的实现绑架了前后端的通信方式(虽然可以override),但另一方面这里数据与通信的模式又具备了Flux的雏形。最后是Router与History。当然除此之外也有extend,noConflict这样的技术辅助函数。总体而言,Backbone的代码小巧,结构清晰,易读易懂,对于初学者切入mv*框架非常适合。
Event模块:
Event模块将暴露以下api:on,off,once,trigger,这4个是我们所熟悉的事件模块/观察者模式的基本api;还有就是listenTo,stopListening,listenToOnce,这是前3个api的反向控制(Ioc)版本。对于两者我们可以这样理解:前者是站着被观察者的角度,需要暴露的api;而后者是站在观察者的角度所需要的api。这样设计的好处是,观察者在调用api进行观察(调用listenTo或listenToOnce)时,在自身保留与观察事物的索引,于是在观察者被销毁时,可以方便的注销自身在被观察者上已注册的回调(通过调用stopListening),从而避免泄露。
在event模块中首先定义的的是eventsApi函数,这一函数的功能是调用传入的iteratee,并传入events,name,callback和opts。
Iteratee是一个执行实际功能的函数,events是一个用来挂载所有事件回调的object,name是事件名称,callback是回调函数,opts则是额外参数。
我们可以看到eventsApi的作用其实只是封装对name的多态性处理,name可以是含有以多个事件名为键的object,可以是空格分隔事件名的的字符串,或是单一事件名的字符串。
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0, names;
if (name && typeof name === 'object') {
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)) {
for (names = name.split(eventSplitter); i < names.length; i++) {
events = iteratee(events, names[i], callback, opts);
}
} else {
events = iteratee(events, name, callback, opts);
}
return events;
};
既然真正的核心是这些传入eventsApi的iteratee,那就让我们来看看这些iteratee以及如何使用它们形成最后的Api:
首先是onApi,这个函数的作用是通过传入的name,callback,在events对象上创建一个键为name(事件名)的数组,并将包含callback和context(回调函数触发时的上下文)以及listening对象的对象推入数组。这里的context和listening是options中取得的,listening(观察)对象中包含了反向控制时所需的信息,将在之后介绍。
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的进一步封装是internalOn。internalOn调用eventsApi并将onApi作为传入的iteratee。同时如果传入了listening对象,则将在被观察者上记录观察者和观察的信息。
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和listenTo了:
on只需简单的调用internalOn:
Events.on = function(name, callback, context) {
return internalOn(this, name, callback, context);
};
而listenTo需要额外处理的便是,建立这个listening对象。Listening对象包含被观察者obj,用于在观察者上记录观察的listeningTo对象,以及计数器count。同一对观察者与被观察者之间的listen对象会被重用,在onApi调用时这个count便会+1来起到计数的作用。同时我们看到观察者和被观察者都会通过_.uniqueId函数产生的唯一id来标识自身。
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];
if (!listening) {
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
}
internalOn(obj, name, callback, this, listening);
return this;
};
我们已经知道了在被观察者上的事件回调是{eventKey1: [...], eventKey2: [...], ...}的格式,因此回调的移除便是通过便利这个_events对象来实现的,需要注意的是,移除侦听时name和callback都是可以缺省的,没有callback则移除_events[name]中包含的所有回调,如果连name都没有则移除所有侦听,这两者在例如观察者会被观察者整个销毁时非常实用。
var offApi = function(events, name, callback, options) {
if (!events) return;
var i = 0, listening;
var context = options.context, listeners = options.listeners;
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];
if (!handlers) break;
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];
}
}
}
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}
if (_.size(events)) return events;
};
这里的remaining数组避免了反复调用slice。同时我们注意到移除侦听时既要比对handler.callback也要比对handler.callback._callback,后者是因为once绑定侦听时使用了包裹过的函数,其_callback指向原函数。
off和stopListening的实现也水到渠成:
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;
};
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) break; //还没有对被观察者的侦听,直接返回
listening.obj.off(name, callback, this); // 调用被观察者的Events.off
}
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
return this;
};
接下来的iteratee是onceMap,它会把原本的callback包装为once,并在once上记录下原callback,以便方便移除侦听(外部只知道侦听了callback,无需了解这个once的存在)。 _.once所包裹生成的函数将保证原函数只会被调用一次,once调用时用offer(name, once)移除这个单次侦听。
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;
};
once和listenToOnce 便很直接了:
Events.once = function(name, callback, context) {
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
return this.on(events, void 0, context);
};
Events.listenToOnce = function(obj, name, callback) {
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
return this.listenTo(obj, events);
};
最后的trigger也无需多言,值得注意的是triggerEvents 中判断了args的长度再调用call,是因为Function#apply的效率较低,在args长度可以预判的情况下尽量使用call,这是一个常见的小技巧。
var triggerApi = function(objEvents, name, cb, 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;
};
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.trigger = function(name) {
if (!this._events) return this;
var length = Math.max(0, arguments.length - 1);
var args = Array(length);
for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
eventsApi(triggerApi, this._events, name, void 0, args);
return this;
};
最后就是在开头加上:
var Events = Backbone.Events = {}; //将Events挂到Backbone上
var eventSplitter = /\s+/; // 事件名可以用空格分隔
结尾加上:
// bind和unbind是on和off的别名 Events.bind = Events.on; Events.unbind = Events.off; // Backbone本身也挂载了Events的api。 _.extend(Backbone, Events);
这样Backbone的Events模块便完成了。
Backbone源码解读(一)事件模块的更多相关文章
- 虎说:bootstrap源码解读(重置模块)
------<!--action-->------ 开场show:前不生“不犹豫”,后半生“不后悔”.今天又逃课,我不后悔 素材:推特公司的前端框架bootstrap(下称bt),解读源码 ...
- BackBone 源码解读及思考
说明 前段时间略忙,终于找到时间看看backbone代码. 正如知友们说的那样,backbone简单.随性. 代码简单的看一眼,就能知道作者的思路.因为简单,所以随性,可以很自由的和其他类库大搭配使用 ...
- jquery源码分析(七)——事件模块 event(二)
上一章节探讨了事件的一些概念,接下来看下jQuery的事件模块. jQuery对事件的绑定分别有几个API:.bind()/.live()/.delegate()/.on()/click(), 不管是 ...
- Webpack探索【16】--- 懒加载构建原理详解(模块如何被组建&如何加载)&源码解读
本文主要说明Webpack懒加载构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack懒加载构建原理. 本文使用的 ...
- Webpack探索【15】--- 基础构建原理详解(模块如何被组建&如何加载)&源码解读
本文主要说明Webpack模块构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack的基础构建原理. 本文使用的W ...
- Abp 审计模块源码解读
Abp 审计模块源码解读 Abp 框架为我们自带了审计日志功能,审计日志可以方便地查看每次请求接口所耗的时间,能够帮助我们快速定位到某些性能有问题的接口.除此之外,审计日志信息还包含有每次调用接口时客 ...
- seajs 源码解读
之前面试时老问一个问题seajs 是怎么加载js 文件的 在网上找一些资料,觉得这个写的不错就转载了,记录一下,也学习一下 seajs 源码解读 seajs 简单介绍 seajs是前端应用模块化开发的 ...
- SDWebImage源码解读之SDWebImagePrefetcher
> 第十篇 ## 前言 我们先看看`SDWebImage`主文件的组成模块: 之概述和使用
尽管Alamofire的github文档已经做了很详细的说明,我还是想重新梳理一遍它的各种用法,以及这些方法的一些设计思想 前言 因为之前写过一个AFNetworking的源码解读,所以就已经比较了解 ...
随机推荐
- FZU 2147 A-B Game(数学)
我们只需要知道这个取完模最大是 a / 2 + 1就可以了,不过超时了几次,换了visual C++才过,oj还真是傲娇啊. #include<iostream> #include< ...
- opencart 图片管理器 500错误
网站点击文件夹打不开返回500错误,通过ftp删除大于1M的图片文件即可
- javascript 基础 onclick(this)用法介绍
http://www.5idev.com/p-javascript_events_onclick.shtml --------------------------------------------- ...
- CodeForces 590A Median Smoothing
构造题. 答案可以o(n)构造出来.首先要发现规律.只有01交替的串才可能变化,变化规律如下: 1开头,长度为偶数(0结尾):变(len-2)/2次 变完后 前半1 后半01开头,长度为奇数(1结尾) ...
- JQuery常用动画实现函数
1.上拉.下拉和切换 slideup().slidedown().slideToggle() JQuery里面的切换太吊了,它自带判断当前显示状态,如果为显示就执行隐藏,如果为隐藏就执行显示. 2.淡 ...
- 《accelerated c++》---------第六章
本章主要讲了算法部分.就是<algoruthm>里面的算法.
- android怎么打开wifi的组播功能
http://android.tgbus.com/Android/tutorial/201204/418987.shtml
- Ubuntu如何备份和恢复系统 - 落花往事的日志 - 网易博客
在 使用Ubuntu之前,相信很多人都有过使用Windows系统的经历.如果你备份过Windows系统,那么你一定记忆犹新:首先需要找到一个备份工 具(通常都是私有软件),然后重启电脑进入备份工具提供 ...
- scp命令和sftp命令
scp帮助命令: man scpscp功能:下载远程文件或者目录到本地,如果想上传或者想下载目录,最好的办法是采用tar压缩一下,是最明智的选择.从远程主机下载东西到本地电脑拷贝文件命令 scp us ...
- ID3决策树预测的java实现
刚才写了ID3决策树的建立,这个是通过决策树来进行预测.这里主要用到的就是XML的遍历解析,比较简单. 关于xml的解析,参考了: http://blog.csdn.net/soszou/articl ...