6

本文已同步在我的博客

在这个reactvue如日中天、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部分实现的,包括:listenTostopListening

listenToon类似,都是监听一个事件,只不过listenTo是监听其他对象的对应事件,而on是监听自身的对应事件。stopListening同理。比如:

a.on('testevent', function(){
    alert('1');
});
a.trigger('testevent');

如果其他对象希望监听atestevent事件呢?则可以通过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() {
        //...
    }
    ...
}

在事件机制的实现部分,除了核心逻辑之外,在对一些方法的使用上,也很考究。为了绑定函数执行的上下文,我们经常会使用applycall这些方法,而源码中多次提到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;
    }
};

有关为什么callapply的效率更高的解释可以参考这篇文章

模型(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事件,可以设置silenttrue

这部分比较容易让人产生疑惑的是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._pendingtrue,这样当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的美好。知其然,还需知其所以然啊~ ~
不然我觉得我可能会一直疑惑为什么要用一套这么复杂的技术栈,异步请求这块写起来还那么麻烦。这么看,坏事也算是好事了吧~~


bupthly
385 声望39 粉丝

竹杖芒鞋轻胜马,一蓑烟雨任平生