Backbone.jsをコードリーディングしたので、メモですー(長いので注意)

2015/06/24時点 versionは1.2.1のソースコードです。行数ではコメント込みで1873行。

社内勉強会で月1で著名なオープンソースのJavaScriptのコードを読み解く会を始めたので、その第一回としてコードリーディングとしてしばしばオススメされているBackbone.jsを選択しました。世間でオススメされる理由は、実装が割とストレートでトリッキーすぎる実装部分などが少ないからだと思います。読んでみても、意外と普通に実装してるなという感想です。

社内のリーディング会では、上からざっと読み進めていって何をやっているか言葉で簡単に説明できるようになることです。ソースコードの中で何をやっているか全然わからないところがなくなれば良いくらいのモチベーションで、debuggerで分岐や値などを実際に追うまではしていません。

以下、読みながら、気になった部分を軽くメモしていきます。


全体の構成

まずは全体のセクションの構成を紹介。

  1. Factory & Module: L10-L32
  2. Setup: L36-L98
  3. Events: L100-L365
  4. Model: L367-L721
  5. Collection: L723-L1152
  6. View: L1154-L1315
  7. Sync: L1317-L1408
  8. Router: L1410-L1509
  9. History: L1511-1813
  10. Helper: L1815-L1869

以下、各セクションごとにソースコードの掲載と軽くメモ程度のコメントを添えて、理解を深めていきます。
(コメントに使っている行数は本体のコードの行数を表しています。)


1. Factory & Module: L10-L32

冒頭のfacetoryを呼び出してloaderの環境に応じてbackboneオブエクトを返す部分です。
読み込みに応じて、AMDスタイル、CommonJSスタイル、scriptタグ形式の3つに対応しており、よくあるスタイルのモジュール提供の仕方だと思います。

  var root = (typeof self == 'object' && self.self == self && self) ||
            (typeof global == 'object' && global.global == global && global);

  // Set up Backbone appropriately for the environment. Start with AMD.
  if (typeof define === 'function' && defined.amd) {
    define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
      // Export global even in AMD case in case this script is loaded with
      // others that may still expect a global Backbone.
      root.Backbone = factory(root, exports, _, $);
    });

  // Next for Node.js or CommonJS. jQuery may not be needed as a module.
  } else if (typeof exports !== 'undefined') {
    var _ = require('underscore'), $;
    try { $ = require('jquery'); } catch(e) {}
    factory(root, exports, _, $);

  // Finally, as a browser global.
  } else {
    root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
  }
  • L12: rootオブジェクトをセットするために、browser環境のwindowとnode環境のglobal、さらにWebWorker環境のselfの対応。nodeやweb workerでbackbone使う意図ってなんだろうって思ったら、まぁmodelをシリアライズして送ってmodelをデシリアライズして使うみたいときには便利かもなと思います。
  • L16: AMD対応 define.amdでチェックできる
  • L24: CommonJS対応 typeof exports !== ‘undefined’ でチェックできる
  • L31: scriptタグ読み込み&globalにセット方式。v1.2からjqueryが必須でなくなったというのは、ここでセットしている root.$に対応するカスタム$をセットできるということ?

2. Setup: L36-L98

  var previousBackbone = root.Backbone;

  // Create a local reference to a common array method we'll want to use later.
  var slice = [].slice;

  // Current version of the library. Keep in sync with `package.json`.
  Backbone.VERSION = '1.2.1';

  // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
  // the `$` variable.
  Backbone.$ = $;

  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
  // to its previous owner. Returns a reference to this Backbone object.
  Backbone.noConflict = function() {
    root.Backbone = previousBackbone;
    return this;
  };

  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
  // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
  // set a `X-Http-Method-Override` header.
  Backbone.emulateHTTP = false;

  // Turn on `emulateJSON` to support legacy servers that can't deal with direct
  // `application/json` requests ... this will encode the body as
  // `application/x-www-form-urlencoded` instead and will send the model in a
  // form param named `model`.
  Backbone.emulateJSON = false;

  // Proxy Underscore methods to a Backbone class' prototype using a
  // particular attribute as the data argument
  var addMethod = function(length, method, attribute) {
    switch (length) {
      case 1: return function() {
        return _[method](this[attribute]);
      };
      case 2: return function(value) {
        return _[method](this[attribute], value);
      };
      case 3: return function(iteratee, context) {
        return _[method](this[attribute], iteratee, context);
      };
      case 4: return function(iteratee, defaultVal, context) {
        return _[method](this[attribute], iteratee, defaultVal, context);
      };
      default: return function() {
        var args = slice.call(arguments);
        args.unshift(this[attribute]);
        return _[method].apply(_, args);
      };
    }
  };
  var addUnderscoreMethods = function(Class, methods, attribute) {
    _.each(methods, function(length, method) {
      if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
    });
  };
  • L41: no conflict用に既にrootに存在するbackboneを保存
  • L44: Arrayのsliceを使うときに配列のインスタンス化のコストを減らすためのキャッシュ?
  • L55: Backbone.noConflictを呼び出すと、既にglobal空間に別のbackboneなどが読み込まれている場合に、新しく今回生成したものを返してくれる
  • L63: emulateHTTPをtrueにすると古いserver環境でHTTPのPUT, DELETE, PATCHメソッドをPOSTに変換するフラグ(詳しくはsyncの部分で)
  • L69: emulateJSONをtrueにするとapplication/json形式のrequestに対応していない古いサーバー環境で代わりにpplication/x-www-form-urlencodedで投げてくれる。詳しくはsync
  • L73: addMethodとaddUnderscoreMethods ここはunderscoreと作者が同じだからこそできる芸当か、結構特殊な感じです。addUnderscoreMethodsで渡すattributeを、実行のたびにunderscoreの第一引数に渡すためのcurry化のようなmethod proxyの実装。あとで出てくる。

3. Events: L100-L365

Backboneのほとんどのクラスの継承元となる肝となるイベントのやりとりを行うための基盤を提供するEventsです!

  var Events = Backbone.Events = {};

  // Regular expression used to split event strings.
  var eventSplitter = /\s+/;

  // Iterates over the standard `event, callback` (as well as the fancy multiple
  // space-separated events `"change blur", callback` and jQuery-style event
  // maps `{event: callback}`), reducing them by manipulating `memo`.
  // Passes a normalized single event name and callback, as well as any
  // optional `opts`.
  var eventsApi = function(iteratee, memo, 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++) {
        memo = iteratee(memo, names[i], name[names[i]], opts);
      }
    } else if (name && eventSplitter.test(name)) {
      // Handle space separated event names.
      for (names = name.split(eventSplitter); i < names.length; i++) {
        memo = iteratee(memo, names[i], callback, opts);
      }
    } else {
      memo = iteratee(memo, name, callback, opts);
    }
    return memo;
  };

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

  // An internal use `on` function, used to 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;
  };

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

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

まず重要なonの部分で一旦区切ります。

  • L123: eventsApiは第一引数にiteratee(iterateされるもの)が受け取ったオブジェクト(memo)に変更を加えて、そのmemoを返すfunctionとなります。あとで出てくるonApiやoffApiがiterateeとして渡されます。分岐しているところはonするときに”click focus”とかの形式やオブジェクトによる形式にも対応するためのものです。
  • L127: void 0 という表現がでてきていますね。これはundefinedを上書きされてしまっている危険を回避するためのものです。’javascript void undefined’とかググると面白い記事がいろいろ出てくると思います。
  • L150: internalOnではあとで登場するonApiをiterateeとして渡し、objectのイベントの情報をobj._eventsに保持していく処理をします。listeningはonを読んだときはundefinedなので、何も保存しないですが、次のlistenToではeventをlistenする対象の情報が渡されます。
  • L167: listenToの実装です。Inversion-of-control versions of `on`とコメントにもありますように、onする対象が自分自身ではなく他人になっています。その他人のオブジェクトの情報をlisteningに入れて後ほどonApiによりhandlersに追加されます。
  • L186: onApiはeventsApiでon/listenToでiterateされます。受け取った各events(memo)に対してcallbackやlisteningなどの情報を配列で保持していきます。

次はonの逆のoffですー

  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);
    }
    if (_.isEmpty(listeningTo)) this._listeningTo = void 0;

    return this;
  };

  // The reducing API that removes a callback from the `events` object.
  var offApi = function(events, name, callback, options) {
    // No events to consider.
    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;
  };
  • L201: offではeventsApiにoffApiをiterateeとして渡しています。onと逆のことをしているだけですね。
  • L212: stopListeningではlistenToで保存していたlisten対象の情報をlisteningを引っ張り出して辿って、offを行います
  • L233: offApiではonApiで保持していたcallbackなどの情報を消していく処理です
  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);
  };

  // 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;
  };
  • L292: 一回きりのcallbackを登録するonceの実装です。またeventsApiを利用してonceMapで受け取ったcallbackを一回だけ実行にするためにunderscoreの_.onceを使っています。_.onceの中では回数を保持しておいて1回呼び出されたら以降実装しないようにしています。具体的には_.partial(_.before, 2);を呼んでいて、partialが返す関数に渡された関数を事前に与えている引数であるbeforeを2という引数とともに呼び出します。beforeの中では単純に–timesして1以下になったらfuncにnullをセットして呼ばれないようにしています。

ここでようやく次はtriggerです。今までonやlistenToは”callbackを登録する”という部分まででした。triggerによって、登録されているcallbackを実行します。

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

  // Handles triggering the appropriate event callbacks.
  var triggerApi = function(objEvents, name, cb, 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;
    }
  };

  // Aliases for backwards compatibility.
  Events.bind   = Events.on;
  Events.unbind = Events.off;

  // Allow the `Backbone` object to serve as a global event bus, for folks who
  // want global "pubsub" in a convenient place.
  _.extend(Backbone, Events);
  • L322: triggerでもeventsApiでtrigeerApiというiterateeを渡しています
  • L334: triggerApiではonやlistenToで保持しておいた_eventsから対応するイベント名を取り出し、triggerEventsの中でcallbackを実行します
  • L348: triggerEventsではなぜか引数の数に応じてcallback.callとcallback.applyを切り替えている最適化のための面白いコードw 引数の数に応じてそれぞれcallを呼んだ方が速いらしい(difficult-to-believeとコメントがありますが、信じられないですねぇ。)
  • L365: Backboneオブジェクト自身にEventsを継承させてGlobal Pub/Subの仕組みを作っていました。知らなかった。。

4. Model: L367-L721

Eventsは大体理解できたでしょうか、次からModelなどの実際にユーザーが触れるオブジェクトに関する実装です。

  var Model = Backbone.Model = function(attributes, options) {
    var attrs = attributes || {};
    options || (options = {});
    this.cid = _.uniqueId(this.cidPrefix);
    this.attributes = {};
    if (options.collection) this.collection = options.collection;
    if (options.parse) attrs = this.parse(attrs, options) || {};
    attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
    this.set(attrs, options);
    this.changed = {};
    this.initialize.apply(this, arguments);
  };

  // Attach all inheritable methods to the Model prototype.
  _.extend(Model.prototype, Events, {

    // A hash of attributes whose current and previous value differ.
    changed: null,

    // The value returned during the last failed validation.
    validationError: null,

    // The default name for the JSON `id` attribute is `"id"`. MongoDB and
    // CouchDB users may want to set this to `"_id"`.
    idAttribute: 'id',

    // The prefix is used to create the client id which is used to identify models locally.
    // You may want to override this if you're experiencing name clashes with model ids.
    cidPrefix: 'c',

    // Initialize is an empty function by default. Override it with your own
    // initialization logic.
    initialize: function(){},

    // Return a copy of the model's `attributes` object.
    toJSON: function(options) {
      return _.clone(this.attributes);
    },

    // Proxy `Backbone.sync` by default -- but override this if you need
    // custom syncing semantics for *this* particular model.
    sync: function() {
      return Backbone.sync.apply(this, arguments);
    },

    // Get the value of an attribute.
    get: function(attr) {
      return this.attributes[attr];
    },

    // Get the HTML-escaped value of an attribute.
    escape: function(attr) {
      return _.escape(this.get(attr));
    },

    // Returns `true` if the attribute contains a value that is not null
    // or undefined.
    has: function(attr) {
      return this.get(attr) != null;
    },

    // Special-cased proxy to underscore's `_.matches` method.
    matches: function(attrs) {
      return !!_.iteratee(attrs, this)(this.attributes);
    },

    // Set a hash of model attributes on the object, firing `"change"`. This is
    // the core primitive operation of a model, updating the data and notifying
    // anyone who needs to know about the change in state. The heart of the beast.
    set: function(key, val, options) {
      if (key == null) return this;

      // Handle both `"key", value` and `{key: value}` -style arguments.
      var attrs;
      if (typeof key === 'object') {
        attrs = key;
        options = val;
      } else {
        (attrs = {})[key] = val;
      }

      options || (options = {});

      // Run validation.
      if (!this._validate(attrs, options)) return false;

      // Extract attributes and options.
      var unset      = options.unset;
      var silent     = options.silent;
      var changes    = [];
      var changing   = this._changing;
      this._changing = true;

      if (!changing) {
        this._previousAttributes = _.clone(this.attributes);
        this.changed = {};
      }

      var current = this.attributes;
      var changed = this.changed;
      var prev    = this._previousAttributes;

      // Check for changes of `id`.
      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];

      // For each `set` attribute, update or delete the current value.
      for (var attr in attrs) {
        val = attrs[attr];
        if (!_.isEqual(current[attr], val)) changes.push(attr);
        if (!_.isEqual(prev[attr], val)) {
          changed[attr] = val;
        } else {
          delete changed[attr];
        }
        unset ? delete current[attr] : current[attr] = val;
      }

      // Trigger all relevant attribute changes.
      if (!silent) {
        if (changes.length) this._pending = options;
        for (var i = 0; i < changes.length; i++) {
          this.trigger('change:' + changes[i], this, current[changes[i]], options);
        }
      }

      // You might be wondering why there's a `while` loop here. Changes can
      // be recursively nested within `"change"` events.
      if (changing) return this;
      if (!silent) {
        while (this._pending) {
          options = this._pending;
          this._pending = false;
          this.trigger('change', this, options);
        }
      }
      this._pending = false;
      this._changing = false;
      return this;
    },

    // Remove an attribute from the model, firing `"change"`. `unset` is a noop
    // if the attribute doesn't exist.
    unset: function(attr, options) {
      return this.set(attr, void 0, _.extend({}, options, {unset: true}));
    },

    // Clear all attributes on the model, firing `"change"`.
    clear: function(options) {
      var attrs = {};
      for (var key in this.attributes) attrs[key] = void 0;
      return this.set(attrs, _.extend({}, options, {unset: true}));
    },

    // Determine if the model has changed since the last `"change"` event.
    // If you specify an attribute name, determine if that attribute has changed.
    hasChanged: function(attr) {
      if (attr == null) return !_.isEmpty(this.changed);
      return _.has(this.changed, attr);
    },

    // Return an object containing all the attributes that have changed, or
    // false if there are no changed attributes. Useful for determining what
    // parts of a view need to be updated and/or what attributes need to be
    // persisted to the server. Unset attributes will be set to undefined.
    // You can also pass an attributes object to diff against the model,
    // determining if there *would be* a change.
    changedAttributes: function(diff) {
      if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
      var old = this._changing ? this._previousAttributes : this.attributes;
      var changed = {};
      for (var attr in diff) {
        var val = diff[attr];
        if (_.isEqual(old[attr], val)) continue;
        changed[attr] = val;
      }
      return _.size(changed) ? changed : false;
    },

    // Get the previous value of an attribute, recorded at the time the last
    // `"change"` event was fired.
    previous: function(attr) {
      if (attr == null || !this._previousAttributes) return null;
      return this._previousAttributes[attr];
    },

    // Get all of the attributes of the model at the time of the previous
    // `"change"` event.
    previousAttributes: function() {
      return _.clone(this._previousAttributes);
    },

    // Fetch the model from the server, merging the response with the model's
    // local attributes. Any changed attributes will trigger a "change" event.
    fetch: function(options) {
      options = _.extend({parse: true}, options);
      var model = this;
      var success = options.success;
      options.success = function(resp) {
        var serverAttrs = options.parse ? model.parse(resp, options) : resp;
        if (!model.set(serverAttrs, options)) return false;
        if (success) success.call(options.context, model, resp, options);
        model.trigger('sync', model, resp, options);
      };
      wrapError(this, options);
      return this.sync('read', this, options);
    },

    // Set a hash of model attributes, and sync the model to the server.
    // If the server returns an attributes hash that differs, the model's
    // state will be `set` again.
    save: function(key, val, options) {
      // Handle both `"key", value` and `{key: value}` -style arguments.
      var attrs;
      if (key == null || typeof key === 'object') {
        attrs = key;
        options = val;
      } else {
        (attrs = {})[key] = val;
      }

      options = _.extend({validate: true, parse: true}, options);
      var wait = options.wait;

      // If we're not waiting and attributes exist, save acts as
      // `set(attr).save(null, opts)` with validation. Otherwise, check if
      // the model will be valid when the attributes, if any, are set.
      if (attrs && !wait) {
        if (!this.set(attrs, options)) return false;
      } else {
        if (!this._validate(attrs, options)) return false;
      }

      // After a successful server-side save, the client is (optionally)
      // updated with the server-side state.
      var model = this;
      var success = options.success;
      var attributes = this.attributes;
      options.success = function(resp) {
        // Ensure attributes are restored during synchronous saves.
        model.attributes = attributes;
        var serverAttrs = options.parse ? model.parse(resp, options) : resp;
        if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
        if (serverAttrs && !model.set(serverAttrs, options)) return false;
        if (success) success.call(options.context, model, resp, options);
        model.trigger('sync', model, resp, options);
      };
      wrapError(this, options);

      // Set temporary attributes if `{wait: true}` to properly find new ids.
      if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);

      var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
      if (method === 'patch' && !options.attrs) options.attrs = attrs;
      var xhr = this.sync(method, this, options);

      // Restore attributes.
      this.attributes = attributes;

      return xhr;
    },

    // Destroy this model on the server if it was already persisted.
    // Optimistically removes the model from its collection, if it has one.
    // If `wait: true` is passed, waits for the server to respond before removal.
    destroy: function(options) {
      options = options ? _.clone(options) : {};
      var model = this;
      var success = options.success;
      var wait = options.wait;

      var destroy = function() {
        model.stopListening();
        model.trigger('destroy', model, model.collection, options);
      };

      options.success = function(resp) {
        if (wait) destroy();
        if (success) success.call(options.context, model, resp, options);
        if (!model.isNew()) model.trigger('sync', model, resp, options);
      };

      var xhr = false;
      if (this.isNew()) {
        _.defer(options.success);
      } else {
        wrapError(this, options);
        xhr = this.sync('delete', this, options);
      }
      if (!wait) destroy();
      return xhr;
    },

    // Default URL for the model's representation on the server -- if you're
    // using Backbone's restful methods, override this to change the endpoint
    // that will be called.
    url: function() {
      var base =
        _.result(this, 'urlRoot') ||
        _.result(this.collection, 'url') ||
        urlError();
      if (this.isNew()) return base;
      var id = this.get(this.idAttribute);
      return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
    },

    // **parse** converts a response into the hash of attributes to be `set` on
    // the model. The default implementation is just to pass the response along.
    parse: function(resp, options) {
      return resp;
    },

    // Create a new model with identical attributes to this one.
    clone: function() {
      return new this.constructor(this.attributes);
    },

    // A model is new if it has never been saved to the server, and lacks an id.
    isNew: function() {
      return !this.has(this.idAttribute);
    },

    // Check if the model is currently in a valid state.
    isValid: function(options) {
      return this._validate({}, _.defaults({validate: true}, options));
    },

    // Run validation against the next complete set of model attributes,
    // returning `true` if all is well. Otherwise, fire an `"invalid"` event.
    _validate: function(attrs, options) {
      if (!options.validate || !this.validate) return true;
      attrs = _.extend({}, this.attributes, attrs);
      var error = this.validationError = this.validate(attrs, options) || null;
      if (!error) return true;
      this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
      return false;
    }

  });

  // Underscore methods that we want to implement on the Model.
  var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
      omit: 0, chain: 1, isEmpty: 1 };

  // Mix in each Underscore method as a proxy to `Model#attributes`.
  addUnderscoreMethods(Model, modelMethods, 'attributes');
  • L377: new Model時の初期化のコード。_uniqueIdの実装がシンプルすぎて良いですね(ただ変数をインクリメントしているだけ)。optionでcollectionを渡しておくと、あとでmodelがdestroyされたときにcollection側でmodelも取り除いてくれるみたいです(知らなかった)。parseはsync時の加工のため。defaultは_.resultで関数でも受け取れるようになっています。
  • L412: toJSONは_.cloneでattributeをcloneしますが、deepではなくshallowクローンなので要注意!!
  • L439: matchesは_.iterateeの中で_.matchesで与えたattrsとマッチするものがあるかどうかを返すのですが、!!_.iteratee(attrs, this)(this.attributes);をreturnしているのが特徴的。!!はobjectやstringを強制的にbooleanに変換するものだと思います。!!””はfalseで、!!”hoge”はtrueになります。
  • L447: setはbackboneをちょっとでもいじった人ならなんとなく知っているであろう実装。validationして、attributesにセットしつつ、前の情報を保持。変更されたものに関しては”change:xxx”がそれぞれtriggerされ、最後changeをtriggerして終わり。
  • L571: fetch, save, destroyはそれぞれGET, POST PUT PATCH, DELETEに対応したsyncを呼ぶもの、かつmodel自身も更新する。waitをtrueでセットすると、サーバーからresponseが返ってきたあとにmodelのset/destroyを行います。
  • L706: _validateでは事前に実装しているvalidatorを実行して、その返り値がtrueだったらOK。それ以外ではinvalidをtriggerする。
  • L718: 不思議な部分。setupで登場したaddUnderscoreMethodsを実際に利用して、model.keysなどでunderscoreのmethodを実行できる。その際に、ここの定義時に渡すmodelのattributeの値を常にunderscoreの第一引数に渡すようになっている。(modelから呼ぶ場合はその引数を省略できる。)underscoreの引数の数に応じて数字を渡しています。これはやはり作者が同じだからこその芸当かと。

以上、とりあえず今回はEventをメインに上から以下のセクションのコードリーディングメモを紹介しました。

  1. Factory & Module: L10-L32
  2. Setup: L36-L98
  3. Events: L100-L365
  4. Model: L367-L721

Backboneは実際に仕事使う案件が多いので、いろいろ新たな発見が多くてリーディング自体おもしろかったです!
次回はCollectionから続きをみていこうと思います。


最後に

コードリーディングをしているとメモをとりたかったり、モバイルで見にくかったりして、もっと良いツールが作れるんじゃないかなと思って、MeetAppというサービスで趣味開発の募集も始めました。

まだ全く具体的なところまでアイデアはできていませんが、ソーシャルコメント機能なども入れたりして、良いのが作れたら社内のコードリーディング会も効率的にできるんじゃないかなという希望的な観測。興味のある方はJOIN US!w

OSSコードリーダー
https://meetapp.tokyo/app/detail/8ddf62ee-9ac2-403e-a39c-5fcb059da705


Categories: JS

Related Posts

JS

Array.prototype.reduce使ってみた

ES5から導入されているArray.prototype.reduceを使ってみたら便利だったので紹介。

JS

NodeJS + Facebook JavaScript SDKを使ってサーバー側で認証チェックをする

Facebook JavaScript SDKを使えばクライアント側でログイン認証の処理が簡潔に実現できますが、自前のWebアプリのサーバー側で提供するAPIを呼びたい時に、API側でも認証のチェックをした方が良いです。 例えば、DBのエントリを削除するよるREST APIを作ったとして、誰もが消せてしまったら困りますよね。エントリを作成した人のみが削除権限を持つような仕組を実装したい場合は、サーバー上のDELETEのAPIで本当にその人であるかの確認を行う必要があります。今回はJavaScript SDKを使ってログインが完了したユーザーのアクセストークン(FB.getLoginStatusを読んだ時に得られるトークン)をNodeJSのexpress環境で作成したREST APIの中でverifyする例を紹介します。

JS

Vue.js v-repeatで文字列を指定するとv-modelが動かない

vue.jsのv-repeatの中でinput要素等にbindingのためのv-modelを指定した時に少しはまったのでメモ。