带你读Backbone源码解读之Events实现


Backbone源码解读

Backbone在流行的前端框架中是最轻量级的一个,全部代码实现一共只有1831行1。从前端的入门再到Titanium,我虽然几次和Backbone打交道但是却对它的结构知之甚少,也促成了我想读它的代码的原始动力。这个系列的文章主要目的是分享Backbone框架中可以用于日常开发的实践参考,力求能够简明扼要的展现Backbone Modal, Controller和Sync这些核心内容,希望能够对大家学习和使用Backbone有一些帮助。

这个系列的文章将包括以下3篇内容:

  • Backbone源码解读之Events实现
  • Backbone源码解读之Router, History实现
  • Backbone源码解读之Model, Collection, Sync实现

本文是它的第一篇《Backbone源码解读之Events实现》

Backbone Events实现

Backbone的Event是整个框架运转的齿轮。它的优美之处在于它是Backbone的一个基础方法,通过_.extend的方法Mixin到Backbone的每一个模块中。

//Model
_.extend(Model.prototype, Events, {
    changed: null,

    //other Model prototype methods
    //...
}

Event的基础概念

事件的管理是一个绑定在Events这个命名空间中的_events对象来实现的。
它的结构是name: [callback functions]的key-callback_array键值对。

this._events = {
    change: [callback_on_change1, callback_on_change2, ....],
    ....
}

当事件发生的时候,event从这个对象中根据事件的名称取得回调函数数组,然后循环执行每个回调函数,也就说明了为什么多次绑定会重复触发多次事件。

Event包括on, off, trigger三个基础方法,其余的所有方法均是对它们的扩展。

on(name, callback, context)

on接受3个参数,包括事件的名称,回调函数和回调函数执行的上下文环境。其中context是可选参数,如果你不是很熟悉JS的执行上下文环境可以暂时不用管它。

抛开所有Backbone的花哨的检查,执行on的操作本质就是向_events中name对应的回调函数数组[callback functions]中Push新的函数。
简单来说代码实现就是这个样子:

Events.on = function(name, callback, context) {
    if (callback) {
      var handlers = events[name] || (events[name] = []);
      handlers.push({callback: callback, context: context, ctx: context || this});
      }
      return this;
};

至于你在看源代码的时候会长很多,那是因为一方面Backbone要处理关于_events以及_events[name]未初始化的两种特殊情况。另一方面eventsApi,onApi这些方法是为了处理on时候你传入的不是一个string类型的名称和一个callback函数所做的条件处理。
例如下面两种方法都是合法的:

//传入一个名称,回调函数的对象
model.on({ 
    "change": on_change_callback,
    "remove": on_remove_callback
});  

//使用空格分割的多个事件名称绑定到同一个回调函数上
model.on("change remove", common_callback);  

但是核心其实都是同一个绑定函数。

值得注意的一点是由于Backbone接受all作为name的参数,并且将回调函数保存在_events.all中,关于它的执行详细可以参考trigger。

off(name, callback, context)

与on不同,off的3个参数都是可选的。

  • 如果没有任何参数的时候,off相当于把对应的_events对象整体清空。

    if (!name && !callback && !context) {
        this._events = void 0;
        return this;
    }
    
  • 如果有name参数但是没有具体清除哪个callback的时候,则把_events[name]对应的内容全部清空。

    if (!callback && !context) {
        delete this._events[name];
        continue;
    }
    
  • 如果还有进一步详细的callback和context的情况下,则进入[callback functions]中进行检查,移除具体一个回调函数的条件非常严苛,必须要求上下文和函数与原来完全一致,也就是说如果传入的callback或者context不是原有on对象的引用,而是复制的话,这里的off是无效的。

    var remaining = [];
    if(
        callback && callback !== handler.callback &&
        callback !== handler.callback._callback ||
        context && context !== handler.context
    ){
        //保留回调函数在数组中
    }
    

trigger(name)

trigger取出name对应的_events[name]以及_event.all中的callback函数。
需要注意的一点是,触发对应名称的callback和all的callback使用了不一样的参数,all的参数中还包含了当前事件的名称。

//当绑定3个以下回调函数的时候Backbone会做如下优化处理,据说这样是可以提高执行效率的。    
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事件使用

从使用上来讲,对一个对象进行事件的on绑定。然后在同一个对象的其他函数执行过程中,或者其他的对象中,触发该对象的trigger方法和对应的事件名称来执行其上绑定的callback函数。

其他辅助函数

接下来再看看Event中进一步定义了哪些其他辅助函数

once(name, callback, context)

效果相同与on,不过对应的callback函数仅执行一次。当然,你同样可以在once绑定的回调函数执行前手动通过off将其移除。

来看看Once的实现,由于添加了执行过程中的移除方法,once在实际实行on的时候使用了如下匿名函数:

var once = _.once(function() {function(){
    self.off(name, once);
    callback.apply(this, arguments);
});
return this.on(name, once, context);

但是细心的你一定发现,保存在_event数组中的函数是once这个匿名函数了。但是用户并不知道Backbone的这些操作,在取消绑定时仍然会使用原来的回调函数来试图解除绑定。上面我们也提到,必须使用完全一致的函数才能够取消绑定,那么为什么还能够成功呢?

这里Backbone做了一个小小的操作,不知道你有没有注意到上面off函数中有这样一行内容?

callback !== handler.callback._callback

既然callback是我们传入的回调函数,那么哪里来的_callback这个属性呢?答案就在once里面。

var once = _.once(function() {...取消绑定,执行callback);
once._callback = callback;

也就是Backbone在返回之前悄悄的为once这个函数添加了一个_callback的属性,用来保存原来的回调函数,这样用户在传入原来的回调函数取消绑定的时候,off会检查函数时候有_callback这个属性和用户传入的函数匹配,同样可以取消绑定。

listenTo(obj, name, callback)、listenToOnce(obj, name, callback)和stopListening(obj, name, callback)

除了将对象本身expose给另一个对象,让另一个对象执行trigger方法触发该对象上绑定的event以外。Event还进一步提供了listenTo系列的方法,执行逻辑正好与on相反。
例如有如下要求,当B对象上发生事件b的时候,触发A对象的callbackOnBEvent函数。

// 使用on的情况下
B.on(“b”, A.callbackOnBEvent)

// 使用listenTo的情况下
A.listenTo(B, “b”, callbackOnEvent);

从实现上看,它门的区别就在于谁负责管理这个事件。第一个模型中,B就像是整个系统的master,负责事件到达的时候的分发,让不同的对象(如A)执行对应的方法。第二个模型中,B更像是一个信息栈,A监听B上发生的事件,并且在对应事件到达的时候触发自身相应的回调函数。两者并无好坏之分,但是从系统架构上来说因为本身回调函数的上下文环境就是A,所以listenTo的方式可能会来的更加自然,而且由A自己来控制什么时候移除回调的执行,也可以让代码的解耦程度更高。

超越Backbone

使用Event方法来处理异步请求让代码的可读性大大增加。如果你的单页面应用恰好使用了Backbone作为前端框架,将Event通过Backbone.Events这个变量暴露出来,你可以使用类似Model扩展的方法

//Your object
_.extend(your_object.prototype, Backbone.Events, {
    //other prototype methods
    //...
}

这样你的Object也就具有了彼此绑定事件、触发事件的能力。

即便你的前端并没有使用Backbone,由于Events并不依赖Backbone的其他部分实现,你完全可以将它放到自己的代码lib中,作为一个基础方法来使用。

类似的方式你也可以经常在Node的后端看到

var util = require("util");
var events = require("events");

function MyStream() {
    events.EventEmitter.call(this);
}

util.inherits(MyStream, events.EventEmitter);

总之,我个人是非常推荐多多使用Event来替代层级的Callback结构。


  1. 根据2015年4月 稳定版本Backbone.js 1.1.2的注释版本。Master上的代码和注释版本稍有出入,哪位大神知道为什么吗?? 


4 responses on “带你读Backbone源码解读之Events实现

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>