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閱讀 230,048評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,414評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,169評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,722評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,465評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,823評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,813評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,000評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,554評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,295評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,513評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,722評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,125評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,430評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,237評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,482評論 2 379

推薦閱讀更多精彩內容

  • 寫在前面 backbone是我兩年多前入門前端的時候接觸到的第一個框架,當初被backbone的強大功能所吸引(當...
    浙大javascript聯盟閱讀 1,153評論 0 5
  • # Backbone入門之事件(Backbone.Events) 本系列前一篇講述了[Backbone入門之視圖]...
    驚鴻三世閱讀 1,391評論 0 3
  • https://nodejs.org/api/documentation.html 工具模塊 Assert 測試 ...
    KeKeMars閱讀 6,361評論 0 6
  • 1.JQuery 基礎 改變web開發人員創造搞交互性界面的方式。設計者無需花費時間糾纏JS復雜的高級特性。 1....
    LaBaby_閱讀 1,364評論 0 2
  • 1.JQuery 基礎 改變web開發人員創造搞交互性界面的方式。設計者無需花費時間糾纏JS復雜的高級特性。 1....
    LaBaby_閱讀 1,195評論 0 1