6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Backbone.js再入門はソースコードリーディングから【その2】

Last updated at Posted at 2015-11-19

Backbone.jsのソースコードを読んでみる

今回はBackboneのEvents周り
色々なBackboneのクラスの継承元となるクラスですね。
基本的にイベントの登録・削除・購読・発行を行うのが役割です。

対象Version

Backbone.js 1.2.3

イベントの登録

  /////////////////////////////////////////////////////////////////////////////////////////////////
  // Events.
  // 色々なBackboneが用意するオブジェクトがObserverパターンを利用することができるようにするための
  // 根幹の処理を提供するためのクラス
  // Model, CollectionなどはすべてEventsを継承している
  //
  // A = {};
  // _.extend(A, Backbone.Events);
  // A.on('foo', function(){
  //     console.log('A::foo');
  // });
  // A.on('bar', function(){
  //     console.log('A::bar');
  // });
  //
  // A.trigger('foo');
  // A.trigger('bar');
  /////////////////////////////////////////////////////////////////////////////////////////////////
  var Events = Backbone.Events = {};

  var eventSplitter = /\s+/;

  // iterateeにはイベント登録・削除・購読・発行などの処理を行うための関数オブジェクトが入ってくる
  // イベントの登録・削除・購読・発行などはすべてこの関数を実行することになっている
  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)) {

      // イベント名をスペースで区切った文字列にしている場合はスペースで区切って複数の名前でイベントを登録することができる
      //
      // ```
      // AAA.on('hoge fuga piyo', function(){
      //     console.log('hello');
      // });
      //
      // AAA.trigger('hoge');
      // AAA.trigger('fuga');
      // AAA.trigger('piyo');
      // AAA.trigger('hoge fuga');
      // AAA.trigger('hoge fuga piyo');
      // ```
      // さて、helloは何回実行されるでしょうか。この辺がかなりとっつきづらいのスペースを用いたイベント名は使わないのが良いかもしれない
      //
      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;
  };

  // イベント登録に必要なのは、イベント名、イベント発火時の処理となるcallback、callback実行時のcontext
  Events.on = function(name, callback, context) {
    return internalOn(this, name, callback, context);
  };

  // 渡されたオブジェクトに対して渡されたイベントを登録する
  // 具体的にはオブジェクトのイベント情報を_eventsに格納している
  // listeningに関しては購読者も登録するべき時は購読者のオブジェクトが入ってくる
  var internalOn = function(obj, name, callback, context, listening) {

    // イベント周りの処理(登録・削除)とかは全部eventsApiに対して、何らかの処理用の関数を渡すことで提供されている。
    obj._events = eventsApi(onApi, obj._events || {}, name, callback, {

        // context   : イベント発行者のcontext
        // ctx       : イベントのcallbackで利用されるcontext
        // listening : イベント購読者のcontext
        context: context,
        ctx: obj,
        listening: listening
    });

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

    return obj;
  };

  // オブジェクトに登録するイベントのオブジェクトを生成して返す
  // {
  //   callback  : Function : イベント発火時の処理
  //   context   : Object   : イベント登録されるオブジェクトのcontext
  //   ctx       : Object   : イベント発火時のcontext
  //   listening : 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;
  };
  • EventsはBackboneが提供するクラスの基底クラス
  • イベントの登録には、イベント名、イベントが発行オブジェクト・イベント発火時の処理・イベント発火時のcontext・監視者(option)が必要
  • スペースを使ったイベント名のイベントの扱いには注意が必要
  • イベントの情報はイベント発行オブジェクトが持つことになる

発行者と購読者


  // オブジェクトに対するイベントを他のオブジェクトに購読させることができる
  // 例えば、ModelのイベントをViewに購読させ、Viewが破棄されるタイミングでModelのイベントを破棄することができる
  // listenToではなく、onを使ってModelのイベントにてViewの参照を渡してしまうとViewを削除してもModel側でViewの参照を持ってしまう(イベントが破棄されない)
  //
  // //////////////////////////////////////////
  // // .listenToを利用した例
  // // Observerパターン
  // //////////////////////////////////////////
  // AAは発行者
  // BBは購読者
  // AA = new Backbone.Model();
  // BB = new Backbone.View();
  // BB.name = 'bb';
  // 
  // BB.listenTo(AA, 'foo', function(){
  //   console.log(this.name);
  // });
  // AA.trigger('foo');
  // 
  // // BB.removeのタイミングでAAに貼られたイベントも開放する
  // BB.remove();
  // AA.trigger('foo');
  // 
  // delete AA;
  // delete BB;
  // console.log('---------------------');
  // 
  // //////////////////////////////////////////
  // // .listenToを使用せず.onで↑と同じことをするとどうなるか
  // //////////////////////////////////////////
  // AA = new Backbone.Model();
  // BB = new Backbone.View();
  // BB.name = 'bb';
  // 
  // AA.on('foo', function(){
  //   console.log(this.name);
  // }, BB);
  // AA.trigger('foo');
  // 
  // BB.remove();
  // AA.trigger('foo');
  // 
  // // 見ての通り、AAのfooイベントにはBBのcontextがまだ存在していることになる
  // // これをZombieViewという。メモリ管理上、非効率である
  // console.log(AA._events.foo[0]);
  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];

    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;
  };
  • listenToを使うとメモリ管理が効率的(この辺に関してはViewクラスの時にもう一回やりそうなので適当)

イベント発火


  // 登録したイベントを発火させるためのメソッド
  // 可変長引数を受けて実行することができる
  Events.trigger =  function(name) {
    if (!this._events) return this;

    var length = Math.max(0, arguments.length - 1);
    var args = Array(length);
    for (var i = 0; i < length; i++) args[i] = arguments[i + 1];

    eventsApi(triggerApi, this._events, name, void 0, args);
    return this;
  };

  // イベントを発火させるためのAPI
  // オブジェクトに登録されていたイベントをガバッと持ってきてnameと呼ばれているイベント名から
  // 発火対象のイベント選定して登録されていたcallbackを実行するだけだが、少し注意が必要
  //
  //////////////////////////////////////////
  // イベント名『all』は特別な動きをする
  //////////////////////////////////////////
  // 
  // AAAA = {};
  // _.extend(AAAA, Backbone.Events);
  // 
  // AAAA.on('hoge', function(){
  //     console.log('hey');
  // });
  // AAAA.on('all', function(){
  //     console.log('hey');
  // });
  // 
  // // さて、heyは何回実行されるでしょうか
  // AAAA.trigger('hoge');
  // AAAA.trigger('all');
  // AAAA.trigger('hey');
  var triggerApi = function(objEvents, name, cb, args) {
    if (objEvents) {
      var events = objEvents[name];
      var allEvents = objEvents.all;
      if (events && allEvents) allEvents = allEvents.slice();

      // まずマッチしたイベントを実行し
      // その後にallで登録されていたイベントを実行する
      if (events) triggerEvents(events, args);
      if (allEvents) triggerEvents(allEvents, [name].concat(args));
    }
    return objEvents;
  };

  // Function.callの方がFunction.applyより早いから変数の数が3までの間はcallを使うようにして静的な最適化を行なっている
  // それ以上の場合はcase分を書ききれないため、applyに頼っている
  // ちなみにBackbone純正のイベントは3までで収まっているようだ
  // addMethod()と同じような感じですかね。
  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;
    }
  };
  • イベント名『all』はどのイベントの時でも敏感に反応してしまう子なので取り扱い注意
  • ここでもまたBackbone芸がみれる

その他API


  // 登録したイベントを削除する
  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;
  };

  // listenToで登録したイベントを外す
  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);
    }
    // void 0はundefinedを返す
    // undefinedが万が一定義済みの場合であってもundefinedを保証することができる
    if (_.isEmpty(listeningTo)) this._listeningTo = void 0;

    return this;
  };

  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];
      }
    }
    if (_.size(events)) return events;
  };

  // 1回ポッキリのイベント登録
  Events.once =  function(name, callback, context) {
    // Map the event into a `{event: once}` object.
    var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
    return this.on(events, void 0, context);
  };

  // 1回ポッキリのイベント購読
  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);
  };

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

  Events.bind   = Events.on;
  Events.unbind = Events.off;


  • この辺あまり読めていないが、まぁそういうこと
  • aliasとして登録されているものも有る

mixin


  // Backbone.直下にもEventsで定義されている機能を生やしている
  // Backbone.Events.on === Backbone.on -> true
  // が成り立つ
  _.extend(Backbone, Events);
  • 実はBackboneオブジェクトにもEventsの機能がある
6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?