本文已同步在我的博客
在这个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中最核心部分的实现机制。可以看到其实现非常的简单,但是对于小型项目来说,确实可以帮我们做一些对数据的维护和管理工作,提高开发效率。但是随着业务逐渐复杂,会越来越发现,backbone所能做的实现有限,而对于数据维护部分也非常不方便,尤其是需要是对多个模块间的通信和数据维护问题。后续我会结合在复杂业务中的使用谈一谈backbone的缺点,以及更优的框架能带来的便利。
说句题外话,虽然去年由于时间原因选择了backbone,这一年基本没有在复杂业务场景中使用react技术栈,都是自己做个小demo练手。但是也正是因为有了使用backbone去写复杂业务的经历,在数据维护上和模块间通信上非常麻烦,以及backbone渲染dom时直接全部更新的会导致的页面渲染性能问题,才更让我感觉react + redux的美好。知其然,还需知其所以然啊~ ~
不然我觉得我可能会一直疑惑为什么要用一套这么复杂的技术栈,异步请求这块写起来还那么麻烦。这么看,坏事也算是好事了吧~~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。