[聚合文章] 小而美的backbone

JavaScript 2018-01-24 11 阅读

本文已同步在 我的博客

在这个 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

listenTo和 on 类似,都是监听一个事件,只不过 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 的美好。知其然,还需知其所以然啊~ ~

不然我觉得我可能会一直疑惑为什么要用一套这么复杂的技术栈,异步请求这块写起来还那么麻烦。这么看,坏事也算是好事了吧~~

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。