Backbone系列篇之Backbone.Events源码解析

本文同步更新于www.devsai.comjavascript

一直想着读读源码,但一直没有找到目标,一些流行的框架,大多代码量很多。java

就像是面对着高耸如云的山峰,抬头望去,就已经没了攀登的勇气。git

俗话说的好,凡事得一步一个脚印,一口吃不出个胖子。github

大框架搞不定,能够短小精悍的类库下手。api

打BOSS前一定要杀掉无数的小怪。数组

而,backbone就是个很是好的选择,加上它的注释也就2000行左右。app

也在网上看到一些对Backbone源码的解析,但或多或少的有如下几个状况:框架

  • 一些Backbone解析,只作了部分就停更了
  • Backbone解析的,据如今已有年代,解析的源码与如今的有略微的出入
  • 对源码的解析,多少带有阅读者的想法

最后一点,也是最重要的一点,并非阅读者的想法不对,
而是想,若是本身去阅读,或许能获得不一样的想法。jsp

并且对于阅读源码的来讲,他从源码中得到的收获,必定是要比写出来的多。函数

我建议你们去看别人对一些源码的解析,更建议本身也去试着读读源码。
这样,本身对源码更深刻理解的同时,还能够对别人作的分析,进行更深层次的探讨。

Backbone.Events 事件机制

本文中会出现部分的源码,点击这里查看完整源码

Events 相关代码有200多行

对外定义的方法有:

代码开始,就先定义了Backbone.Events,这是为何呢

由于Backbone的其余部分对象都是继承了Events,也是就说,Backbone.Model,Backbone.Collection,Backbone.View,Backbone.Router

均可以使用Events的属性。

Backbone.Events也可使用在任何的对象上,就像这样:var o=_.extend({},Backbone.Events);

而后o对象,就能够为所欲为的作到订阅/发布了。

上述的API方法能够分三部分:

  • 绑定事件 on,listenTo,once,bind

首先,onbind是彻底同样的,只是取了个别名。方便你们的使用习惯。

listenTo官方说明是对on控制反转。如何反转,后面具体说明。

once就很好理解了,注册的事件只执行一次,完了自动解绑。这也就是为何下面的解绑方法中没有对其解绑的动做了。(一次性筷子,用完就扔,不须要洗)

  • 解绑事件 off,stopListening,unbind

一样的offunbind除了方法名不一样外,做用彻底同样。

stopListening也是用来解绑的,但它比较厉害了,对调用对象解绑解的不折不扣。

  • 触发事件 trigger

经过此方法能够触发单个或同时触发多个事件。trigger(eventname), 第一个参数为事件名,其余的参数为传给事件执行函数的参数。

listenTo(on的控制反转)

object.listenTo(other, event, callback)复制代码

让 object 监听 另外一个(other)对象上的一个特定事件。不使用other.on(event, callback, object),而使用这种形式的优势是:listenTo容许 object来跟踪这个特定事件,
而且之后能够一次性所有移除它们。callback老是在object上下文环境中被调用。

这里有个概念叫Inversion of Control(IoC控制反转)
这是种主从关系的转变,一种是A直接控制B,另外一种用控制器(listenTo方法)间接的让A控制B。

经过listenTo把本来other主导绑定监听事件,变成了由object主导绑定监听事件了。

on比较

从功能上来讲,on,listenTo是同样的。

来看个例子:

var changeHandler = function(){}

model.on('change:name',changeHandler,view);复制代码

或者能够这样

view.listenTo(model,'change:name',changeHandler);复制代码

两种方式的做用是同样的,当model的name发生改变时,调用view中的方法。

可当view中不止有一个model时呢

功能上来说,仍是无差异,但若是想要当离开页面时view须要销毁,view中model绑定的事件也须要注销时,看看两种绑定方式,对面这问题时会怎么办

on的解绑

var view = {
    changeName :function(name){
       //doing something
    }
}
model.on('change:name',view.changeName,view);
model2.on('change:name',view.changeName,view);

//view离开时,model如何解绑
model.off('change:name',view.changeName,view);
model2.off('change:name',view.changeName,view);复制代码

有多个model的话,须要进行屡次的解绑操做。

再来看看listenTo的解绑

view.listenTo(model,'change:name',view.changeName);
view.listenTo(model2,'change:name',view.changeName);

//解绑
view.stopListening();复制代码

并不须要作更多的操做就能把view相关的监听事件给解绑。

而经过查看stopListening

Events.stopListening = function(obj, name, callback) {
    var listeningTo = this._listeningTo;
    if (!listeningTo) return this;

    var ids = obj ? [obj._listenId] : _.keys(listeningTo);

    for (var i = 0; i < ids.length; i++) {
      var listening = listeningTo[ids[i]];

      // If listening doesn't exist, this object is not currently
      // listening to obj. Break out early.
      if (!listening) break;

      listening.obj.off(name, callback, this);
    }

    return this;
  };复制代码

内部执行了屡次的.off(name, callback, this),至关于内部给作了用on绑定后的解绑操做。

深刻了解listenTo

先举个例子,执行view.listenTo(model,'change',changeHandler), 执行过程看下面注释:

Events.listenTo = function(obj, name, callback) {
    // obj = model
    if (!obj) return this;    

    // obj._listenId 不存在,执行 id = (obj._listenId = _.uniqueId('l')) == 'l1'
    var id = obj._listenId || (obj._listenId = _.uniqueId('l'));  

    // this._listeningTo 不存在,执行 listeningTo = (this._listeningTo = {})
    var listeningTo = this._listeningTo || (this._listeningTo = {});

    // listening = this._listeningTo[obj._listenId] : undefined == ({})['l1']
    var listening = listeningTo[id];

    // true 执行条件语句
    if (!listening) {
      // this._listenId == undefined , thisid = (this._listenId = _.uniqueId('l')) == 'l2'
      var thisId = this._listenId || (this._listenId = _.uniqueId('l'));

      // this._listeningTo[obj._listenId] = {....}
      listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
    }

    internalOn(obj, name, callback, this, listening);
    return this;
  };复制代码

上述代码执行中,会调用内部函数onApi(在internalOn内调用),执行handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});

执行完后:

model._listenId = 'l1'
view._listenId = 'l2'
view._listeningTo = {'l1' : {obj:model,objId : 'l1',id : 'l2',listeningTo: view._listeningTo,count : 0}}
model._listeners = {'l2' : view._listeningTo['l1'] }
model._event = {'change':[{callback: changeHandler, context: view, ctx: view, listening: view._listeningTo['l1']}]}复制代码

view._listeningTo 的key 为model._listenId , 也就是说,增长一个model实例,就会增长一个key,
例如再执行:view.listenTo(model2,'change',changeHandler)

因此经过_listeningTo属性,可以知道view与多少个model有关联。

这样,当执行view.stopListening()时,就能把model,model2上的监听事件所有移除了。

一样的,
model._listeners的key 为view._listenId, 例如:view2.listenTo(model,'change',changeHandler),
那么会再生成一个view2._listenId, model._listeners的key将多一个。

为何Backbone.Events会有listenTostopListening

在不少的类库中使用的事件机制都是没有这两个方法的功能。

这两个方法更像是专为view,model而生的。
经过这两个方法能够方便的对view相关的对象监听事件进行跟踪,解绑。

事件对象上的_events

如上的model._events,咱们来分析下它里面有些什么:

model._events它是一个对象 : { key1 : value1, key2 : value2 , key3 : value3 ....}。以事件名为key, value则是一组组数,数组内的每一元素又是一个对象

元素中的对象内容以下:

  • callback 事件的回调函数
  • context 回调函数的上下文对象(即当调用on时,为context参数,当调用view.listenTo(....)时,为调用的对象如:view。)
  • ctx 为context ,当context不存在时,为被监听的对象,如:model.on(...)或view.on(model,...)中的model
  • listening 其实就是view._listeningTo中的某个属性值,能够当作: listening == view._listeningTo['l1']

contextctx

如上所述,每一个元素里的 contextctx几乎同样,那为何须要两个属性呢。

经过阅读off方法及trigger方法就会知道,上面两属性在这两个方法中分别被使用了。

off里须要对context进行比较决定是否要删除对应的事件,因此model._events中保存下来的 context,必须是未作修改的。

trigger里在执行回调函数时,须要指定其做用域,当绑定事件时没有给定做用域,则会使用被监听的对象当回调函数的做用域。

好比下面的代码:

var model = {  name : 'devsai'  }
var changeHandler = function(){ console.log(this.name)}
_.extend(model,Backbone.Events)
model.on('change',changeHandler)
model.trigger('change');  // print : devsai

model.off();
var context = { name : 'SAI'}
model.on('change',changeHandler,context)
model.trigger('change');  // print : SAI

model.off()
var view = { name : 'SAI listenTo' }
_.extend(view,Backbone.Events)
view.listenTo(model,'change',changeHandler)
model.trigger('change')   // print : SAI listenTo复制代码

在调用trigger时,可能会执行这部分代码

(ev = events[i]).callback.call(ev.ctx)复制代码

但这边,这种写法我是有疑惑的,就如 ev.ctx在没有context的状况下, ctx 才是obj(即被监听的对象),
为什么不去掉ctx属性, 而后在trigger时,作context判断

例如把代码改为:

(ev = events[i]).callback.call(ev.context || ev.obj)复制代码

这样ctx属性就能够不去定义了。理解起来更直观。

内部函数 eventsApi

eventsApi是内部的函数,全部对外的接口,都会直接或间接的调用它。复用率极高。

eventsApi主要是干什么的呢。

var eventsApi = function(iteratee, events, name, callback, opts) {
    var i = 0, names;
    if (name && typeof name === 'object') {
      // Handle event maps.
      if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
      for (names = _.keys(name); i < names.length ; i++) {
        events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
      }
    } else if (name && eventSplitter.test(name)) {
      // Handle space-separated event names by delegating them individually.
      for (names = name.split(eventSplitter); i < names.length; i++) {
        events = iteratee(events, names[i], callback, opts);
      }
    } else {
      // Finally, standard events.
      events = iteratee(events, name, callback, opts);
    }
    return events;
  }复制代码

经过调用对外方法(如on,listenTo,once...)传入的是'change update',callback{'change':callback,'change update':callback},而最终指向的内部API函数为单个事件:eventName,callback

因此简单说,该方法对多事件进行解析拆分,遍历执行单个'eventname',callback

下面来具体说说eventsApi的参数

iteratee

是个函数,根据调用的对外接口不一样,该函数也不一样。

如:作绑定iteratee = onApi , onceMap; 作解绑 iteratee = offApi; 作触发 iteratee = triggerApi


events

已有事件的集合,当前事件对象上绑定的全部事件


name

事件名,来源于各对外接口传入的name

有两种类型,string (例如:"change","change update"),map object (例如:{"change":function(){}, "update change":function(){}})


callback

回调函数,来源于各对外接口传入的callback,但它也不必定老是回调函数,当name为object时,callbcak多是context。


opts

根据调用的接口不一样,有如下几种状况

  • on ,listenTo,off ,调用这三个接口时 opts是个对象,
    存放着{context: context,ctx: obj,listening: listening }
    obj为被监听的对象(off时不须要),context为回调函数的上下文 , listening ,调用listenTo时存在。
  • once,listenToOnce , 调用这两个接口时 opts是个函数(作解绑操做)
  • trigger , 此时opts是个数组(args,为触发事件传时回调函数的参数)

内部函数 triggerEvents

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内部大部分的事件最多只有3个参数,对事件调用进行了优化,
先尝试使用call调用,尽可能的不去使用apply调用,以此达到优化的目的。

这里有对call,apply性能对比测试 jsperf.com/call-apply-…

最后

欢迎你们来一块儿探讨backbone,因为我的能力有限,若有描述不妥或不对之处,请及时联系我或评论我。

若是喜欢这篇文章,帮忙点个赞支持下。

若是但愿看到后续其余Backbone源码解析文章,请点下关注,第一时间得到更多更新内容。

相关文章
相关标签/搜索