Events类在backbone库中扮演着很重要的角色,backbone的View与Model都用到了它。

目前项目中使用到的backbone是v1.1.2版,下面的代码分析也是基于此版本。官网最新版是v1.3.2,相比较多了几个方法,核心的东西还是一样的。

Events提供的方法:

on,off,trigger,once,listenTo,stopListening,listenToOnce

这里重点介绍前面3个方法。

on方法

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

我们看到内部维护着event map,名为_events。map的key是事件名,也就是传进来的参数name;map的value是一个数组,输入参数callback和context组成一个成员。

到这里,大概也就明白Events的实现机制了。

关于on方法里调用的eventsApi,只有在name是object或是有空格分割的事件时才有用,留意一下它的牛逼写法:

for (var key in name) {
  obj[action].apply(obj, [key, name[key]].concat(rest));
}
...

off方法

off: function(name, callback, context) {
  var retain, ev, events, names, i, l, j, k;
  if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
  if (!name && !callback && !context) {
    this._events = void 0;
    return this;
  }
  names = name ? [name] : _.keys(this._events);
  for (i = 0, l = names.length; i < l; i++) {
    name = names[i];
    if (events = this._events[name]) {
      this._events[name] = retain = [];
      if (callback || context) {
        for (j = 0, k = events.length; j < k; j++) {
          ev = events[j];
          if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
              (context && context !== ev.context)) {
            retain.push(ev);
          }
        }
      }
      if (!retain.length) delete this._events[name];
    }
  }

  return this;
},

如果name, callback, context都是null,则销毁_events.

如果name存在,则遍历_events[name]对应的队列,队列中每个单元分别与输入参数callback和context比较,查找移除项:

a. 两项都相等才移除;

b. 如果参数传进来的callback和context其中之一为null,则与另一个比较,相等就移除。

trigger方法

trigger: function(name) {
  if (!this._events) return this;
  var args = slice.call(arguments, 1);
  if (!eventsApi(this, 'trigger', name, args)) return this;
  var events = this._events[name];
  var allEvents = this._events.all;
  if (events) triggerEvents(events, args);
  if (allEvents) triggerEvents(allEvents, arguments);
  return this;
},

从_events中取出callbacks,看trigger里的处理:

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;
  }
};

可以看到,队列里callback一一执行,需要注意的是,callback最多支持3个参数。

总结

通过代码了解到这些:

  • Events内部维护着一个event map,event与callback是一对多的关系。
  • callback执行顺序按照监听顺序。
  • callback参数个数有限制,最多支持3个。

还有很重要的一点:上面的实现会导致循环引用!比如on/off方法没成对调用等情况。

一直认为javascript是引用计数来管理内存的。谨慎起见,还得再了解下javascript的内存管理方式。

这里找到了介绍javascript管理内存的方法,里面讲了两点:

  • 引用计数垃圾收集

  • 标记-清除算法

了解到标记-清除算法是浏览器帮着做的,这下放心点了。

其它语言也有很多库用相似的方法实现了事件通知机制,C++和objective-c通过弱引用的方式解决循环引用问题,javascript不支持弱引用,hold不住的内存问题只能交给浏览器了。


作者 侯振永
写于2016 年 3月 24日