Backbone Events源码学习

Backbone Events源码学习

写在前面

backbone作为mvc框架在当前前端开发中已经有点过时了,个人感觉还是有点笨重,不够轻巧吧。但由于其实很多项目还依赖backbone,另外其MVC框架的设计思想也值得借鉴,源码2000行不到的长度,值得一读。

Events用途

Backbone.Events在Backbone中承载着事件机制的角色,可以理解为一条事件总线,不同的元素可以通过触发(自身/其他元素)事件、监听(自身/其他元素)事件来实现代码的解耦(不必在一个元素的事件监听器,如jquery的click回调,中处理其他元素的变化),不过这种代码式的监听比起后来vuejs声明式监听(watch、computed)还是要繁琐和复杂不少。

除了提供了Backbone使用者监听、触发事件的事件总线外,Backbone内部Model、Collection也依赖事件总线进行增删查改等本地以及与服务器的数据交互。

Events在Backbone中的定位

事件总线,可以减轻不同元素之间的耦合度。

Events使用示例

<html>
    <head>
    </head>
    <body>
        <div class="a">
            <span class="text">原始A文案</span>
            <button class="btn">按钮a(同时监听b按钮)</button>
        </div>
        <br/>
        <br/>
        <div class="b">
            <span class="text">原始B文案</span>
            <button class="btn">按钮b</button>
        </div>
        <br/>
        <br/>
        <div class="c">
            <button class="btn">按钮c(只监听一次的事件)</button>
        </div>
        <script type="text/javascript" src="underscore-min.js">
        </script>
        <script type="text/javascript" src="./jquery-3.1.1.min.js">
        </script>
        <script type="text/javascript" src="./backbone-min.js">
        </script>
        <script>
            var textA = $(".a .text");
            var textB = $(".b .text");
            _.extend(textA, Backbone.Events);
            _.extend(textB, Backbone.Events);

            $(".a .btn").click(function(){
                textA.trigger("click");
            });

            textA.on("click", function(){this.html("a按钮被点击")});
            textB.listenTo(textA, "click", function(){$(".b .text").html("监听到a文案被修改");});
            var listener = _.extend({}, Backbone.Events);
            listener.once("click", function(){alert("自己被点击");});
            listener.listenToOnce(textA, "click", function(){alert("监听到a文案被修改");});

            $(".c .btn").click(function(){
                listener.trigger("click");
            });
        </script>
    </body>
</html>

上面的例子分别实现了A文字区域监听A按钮点击事件,B文字区域监听A按钮点击事件和非dom对象监听一次按钮事件。

Event的源码实现

下面是理解Events实现的重头戏,源码剖析。

Events可供外部调用的api有如下几个:on/listenTo/off/stopListening/once/listenToOnce/trigger/bind/unbind 。(bind和unbind是on和off的alias)

on、off是监听/解除监听自身的事件,listenTo和stopListening是监听/解除监听其他对象的事件,像obj.trigger的调用能够触发obj的某个事件。

内部api

Events底层通过iternalOn/onceMap/onApi/offApi/eventsApi实现。其中eventsApi是最为基础的一个函数,它负责遍历传入的事件(支持单个事件/空格分隔的多个事件/jquery风格的map结构的事件,如:{event:callback})

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

eventsApi做的事情很简单,将name拆分(如果有多个event事件名的话),然后对每个event调用参数里的iteratee方法。(传入的iteratee是个方法名)

另外,如果采用jquery的风格传入map结构的name,则要讲opts的context设置为回调函数。(这个相当于是callback执行的this指针)

绑定一个对象的事件监听

下面我们来看如何实现监听自身的事件。

// Bind an event to a `callback` function. Passing `"all"` will bind
  // the callback to all events fired.
  Events.on = function(name, callback, context) {
    return internalOn(this, name, callback, context);
  };

  // Inversion-of-control versions of `on`. Tell *this* object to listen to
  // an event in another object... keeping track of what it's listening to
  // for easier unbinding later.
  Events.listenTo = function(obj, name, callback) {
    if (!obj) return this;
    var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
    var listeningTo = this._listeningTo || (this._listeningTo = {});
    var listening = listeningTo[id];

    // This object is not listening to any other events on `obj` yet.
    // Setup the necessary references to track the listening callbacks.
    if (!listening) {
      var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
      listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
    }

    // Bind callbacks on obj, and keep track of them on listening.
    internalOn(obj, name, callback, this, listening);
    return this;
  }; 

  // Guard the `listening` argument from the public API.
  var internalOn = function(obj, name, callback, context, listening) {
    obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
      context: context,
      ctx: obj,
      listening: listening
    });

    if (listening) {
      var listeners = obj._listeners || (obj._listeners = {});
      listeners[listening.id] = listening;
    }

    return obj;
  };

on其实直接做了一层proxy转发到了interalOn函数内,然后通过onApi的调用来完成对事件的监听。

// The reducing API that adds a callback to the `events` object.
  var onApi = function(events, name, callback, options) {
    if (callback) {
      var handlers = events[name] || (events[name] = []);
      var context = options.context, ctx = options.ctx, listening = options.listening;
      if (listening) listening.count++;

      handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
    }
    return events;
  };

onApi的流程则是首先判断回调函数是否为空,非空才做处理。

每个对象都有一个_events 属性来记录自己监听了哪些事件。(是一个键值对属性,键为事件名,值为一个列表),列表里的每个元素表示一个处理器,包含了回调函数、context、ctx、listening几个属性。其中listening表示谁在监听这个事件,也就是下一节的内容。

总结:实际上监听事件的过程就是将封装好的callback信息添加到对象_events属性对应事件名的队列中的过程。

对象A对对象B的事件监听

下面我们看下对其他对象事件的监听实现。

// Inversion-of-control versions of `on`. Tell *this* object to listen to
  // an event in another object... keeping track of what it's listening to
  // for easier unbinding later.
  Events.listenTo = function(obj, name, callback) {
    if (!obj) return this;
    var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
    var listeningTo = this._listeningTo || (this._listeningTo = {});
    var listening = listeningTo[id];

    // This object is not listening to any other events on `obj` yet.
    // Setup the necessary references to track the listening callbacks.
    if (!listening) {
      var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
      listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
    }

    // Bind callbacks on obj, and keep track of them on listening.
    internalOn(obj, name, callback, this, listening);
    return this;
  };

首先会判断一下当前自己是否已经监听了B对象。如果没有,则封装B对象的信息并添加到listeningTo队列中。

然后直接调用刚刚的interalOn函数,与之前不同的是,需要传入listening对象,并将context改为this。这样当B对象相应事件发生的时候就会调用callback,并且this指针会指向A对象。(真正生效的this其实是一个ctx的内部属性,它的值为context||obj, 即以传入的优先,如果没有传入则是对象本身)

一次性的监听事件

还有一类事件比较特殊,就是回调一次就不再监听的事件。

// Bind an event to only be triggered a single time. After the first time
  // the callback is invoked, its listener will be removed. If multiple events
  // are passed in using the space-separated syntax, the handler will fire
  // once for each event, not once for a combination of all events.
  Events.once = function(name, callback, context) {
    // Map the event into a `{event: once}` object.
    var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
    if (typeof name === 'string' && context == null) callback = void 0;
    return this.on(events, callback, context);
  };

  // Inversion-of-control versions of `once`.
  Events.listenToOnce = function(obj, name, callback) {
    // Map the event into a `{event: once}` object.
    var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
    return this.listenTo(obj, events);
  };

  // Reduces the event callbacks into a map of `{event: onceWrapper}`.
  // `offer` unbinds the `onceWrapper` after it has been called.
  var onceMap = function(map, name, callback, offer) {
    if (callback) {
      var once = map[name] = _.once(function() {
        offer(name, once);
        callback.apply(this, arguments);
      });
      once._callback = callback;
    }
    return map;
  };

通过onceMap生成一个jquery风格的map,其实是对我们传入的callback进行了一层装饰。在事件回调的过程中,首先解除监听,然后继续原有的业务逻辑。

把调用一次和解除的逻辑通过装饰模式结合在一起,省去了业务对特定逻辑的开发。

事件触发回调机制

每个Events对象内部有一个_events对象,用于保存当前对象监听的事件。当外部通过trigger触发事件时,内部实现如下:

// Handles triggering the appropriate event callbacks.
  var triggerApi = function(objEvents, name, callback, args) {
    if (objEvents) {
      var events = objEvents[name];
      var allEvents = objEvents.all;
      if (events && allEvents) allEvents = allEvents.slice();
      if (events) triggerEvents(events, args);
      if (allEvents) triggerEvents(allEvents, [name].concat(args));
    }
    return objEvents;
  };

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

由于Events提供了对象监听所有事件的功能,如果obj.on('all', function(){}) 这种形式可以处理对象的所有事件,另外事件回调的参数会得到事件名。

很多Backbone内部的trigger事件都带三个参数,这里Events也提供了事件回调接收多个参数的能力。

解除监听

最后讲讲如何解除监听,实际上我觉得这也是Events最难懂的一部分。

// Remove one or many callbacks. If `context` is null, removes all
  // callbacks with that function. If `callback` is null, removes all
  // callbacks for the event. If `name` is null, removes all bound
  // callbacks for all events.
  Events.off = function(name, callback, context) {
    if (!this._events) return this;
    this._events = eventsApi(offApi, this._events, name, callback, {
      context: context,
      listeners: this._listeners
    });
    return this;
  };

  // Tell this object to stop listening to either specific events ... or
  // to every object it's currently listening to.
  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;
  };

  // The reducing API that removes a callback from the `events` object.
  var offApi = function(events, name, callback, options) {
    if (!events) return;

    var i = 0, listening;
    var context = options.context, listeners = options.listeners;

    // Delete all events listeners and "drop" events.
    if (!name && !callback && !context) {
      var ids = _.keys(listeners);
      for (; i < ids.length; i++) {
        listening = listeners[ids[i]];
        delete listeners[listening.id];
        delete listening.listeningTo[listening.objId];
      }
      return;
    }

    var names = name ? [name] : _.keys(events);
    for (; i < names.length; i++) {
      name = names[i];
      var handlers = events[name];

      // Bail out if there are no events stored.
      if (!handlers) break;

      // Replace events if there are any remaining.  Otherwise, clean up.
      var remaining = [];
      for (var j = 0; j < handlers.length; j++) {
        var handler = handlers[j];
        if (
          callback && callback !== handler.callback &&
            callback !== handler.callback._callback ||
              context && context !== handler.context
        ) {
          remaining.push(handler);
        } else {
          listening = handler.listening;
          if (listening && --listening.count === 0) {
            delete listeners[listening.id];
            delete listening.listeningTo[listening.objId];
          }
        }
      }

      // Update tail event if the list has any events.  Otherwise, clean up.
      if (remaining.length) {
        events[name] = remaining;
      } else {
        delete events[name];
      }
    }
    return events;
  };

解除监听最终都由offApi实现。如果没有传递任何参数,则会解除该对象所有事件的监听。

如果传递了,则在_events属性中取出相关的监听器队列,然后比较callback函数跟传入的callback函数(这里针对只监听一次的once监听器还延伸了一个_callback属性的概念),如果不相等则将监听器放入remain队列。否则则删掉相应的监听。

Reference

  1. Backbone官网
  2. Backbone框架剖析
  3. 深入理解Backbone事件机制
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 写在前面 backbone是我两年多前入门前端的时候接触到的第一个框架,当初被backbone的强大功能所吸引(当...
    浙大javascript联盟阅读 1,131评论 0 5
  • # Backbone入门之事件(Backbone.Events) 本系列前一篇讲述了[Backbone入门之视图]...
    惊鸿三世阅读 1,372评论 0 3
  • https://nodejs.org/api/documentation.html 工具模块 Assert 测试 ...
    KeKeMars阅读 6,313评论 0 6
  • 1.JQuery 基础 改变web开发人员创造搞交互性界面的方式。设计者无需花费时间纠缠JS复杂的高级特性。 1....
    LaBaby_阅读 1,330评论 0 2
  • 1.JQuery 基础 改变web开发人员创造搞交互性界面的方式。设计者无需花费时间纠缠JS复杂的高级特性。 1....
    LaBaby_阅读 1,167评论 0 1