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

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

今回はBackboneのView

# 対象Version

Backbone.js 1.2.3

# コンストラクタまわり

```js

  // 引数にoptionsを渡すことでクラス側で定義しているメソッドやプロパティをOverrideすることができる
  // ただし、OverrideできるものはviewOptionsに定義されているkey名のものに限っている
  // コンストラクタは関数は触らないで、newの振る舞いを変えるにはModelと同じく、initializeをオーバーライドさせる
  var View = Backbone.View = function(options) {

    // view1, view2みたいな感じになるようにインスタンスに対して一意なコードを割り振っている
    this.cid = _.uniqueId('view');

    // 特定のkeyのもののみ、オーバーライドされることを許可している
    _.extend(this, _.pick(options, viewOptions));

    // DOM周りのセットアップやイベントの登録などを行う
    this._ensureElement();

    // Modelと同じくinitializeを最後に実行している
    // Modelと同じくプレーンな状態ではinitializeは何も評価しないし、何も返さないのでユーザーが定義する
    this.initialize.apply(this, arguments);
  };

  // イベントのkeyをイベント名とセレクタ名で分割するための正規表現オブジェクト
  var delegateEventSplitter = /^(\S+)\s*(.*)$/;

  // コンストラクタの引数でOverrideが可能なオブジェクトのkey名
  var viewOptions = [
    // Viewで利用するModel
    'model',
    // Viewで利用するCollection
    'collection',
    // View自身のDOM要素のセレクタを定義する
    // コンストラクタ内でDOMのオブジェクトに変換される
    // DOMをセレクタを用いて取得しなくてもよく、id, classNameなどを利用して動的にDOMを構築することもできる
    'el',
    // View自身のDOM要素のrootのDOMに付与するID
    // ※ elの方が優先して使われる
    'id',
    // View自身のDOM要素のrootのDOMに付与するプロパティ
    // ※ elの方が優先して使われる
    'attributes',
    // View自身のDOM要素のrootのDOMに付与するクラス名
    // ※ elの方が優先して使われる
    'className',
    // View自身のDOM要素のrootのDOMに付与するタグ名
    // ※ elの方が優先して使われる
    'tagName',
    // Viewのイベントのkey/value
    'events'
  ];

```


- コンストラクタの引数optionsを使って特定のプロパティやメソッドをOverrideすることができる
- initializeはユーザーが定義してどうのこうのってところはModelと同じような感じ


# 他

```js

  _.extend(View.prototype, Events, {

    // デフォルトでは『<div>』をViewのDOM要素して扱うことになる。
    tagName: 'div',

    // View自身がもつDOM要素に対してのjQuery.fn.findのショートカット
    $: function(selector) {
      return this.$el.find(selector);
    },

    // ユーザーが定義する初期化処理を実装するところ
    // Modelの時と同じようなのり
    initialize: function(){},

    // DOMの要素を画面に出力するための実装を行うところ
    // 生のBackboneだとrenderの内容を常にユーザーが定義する必要がある
    // return this;をすることが一般的なようだ
    render: function() {
      return this;
    },

    // DOMからViewを削除するための処理
    remove: function() {

      // DOMからViewのHTML要素を削除する
      // 具体的にはjQueryのAPIが実行されるだけである
      this._removeElement();

      // Viewが購読しているイベントを全て破棄する。
      // stopListeningをせずに、view = null;などをしてViewのクラスを消してしまうと、イベント発行元に購読者としてのViewのインスタンスが残ってしまうので注意すること。ようはイベントをその後も拾い続けてしまう。
      // .stopListeningはBackbone.Eventsで実装されているものであり、Backbone.ViewはBackbone.Eventsを継承している
      this.stopListening();
      return this;
    },

    // DOMからViewのHTML要素を削除する
    _removeElement: function() {
      this.$el.remove();
    },

    // Viewの要素を変更し、イベント周りを均す
    setElement: function(element) {
      // Viewのイベントを一旦剥がす
      this.undelegateEvents();

      // this.elの書き換えを行う
      this._setElement(element);

      // Viewのイベントを付け直す
      this.delegateEvents();
      return this;
    },

    // this.elの要素の書き換えを行う
    // this.el: Viewの要素のDOMオブジェクト, またはセレクタ
    // this.$el: Viewの要素のjQueryオブジェクト
    // 単純に$elは$(el)のショートカットみたいな扱いになります。
    _setElement: function(el) {
      this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
      this.el = this.$el[0];
    },

    // this.eventsに記載されているイベントの内容をViewに登録させるための処理
    // this.eventsはkey/valueのオブジェクトでもいいし、key/valueのオブジェクトを返すような関数でもOK
    delegateEvents: function(events) {

      // 定義されているeventsを持ってくる
      events || (events = _.result(this, 'events'));
      if (!events) return this;

      // イベントの登録前に一旦イベントを外して均す
      this.undelegateEvents();

      for (var key in events) {
        // イベントのcallback関数はViewのコンテキスト上に存在する関数名を指定するのもOKだけど、関数そのものを指定することもできる
        // {a: 'alistener', b: function(){...}} が混ざってもいいということ
        var method = events[key];
        if (!_.isFunction(method)) method = this[method];
        if (!method) continue;

        // イベントのkeyを分割する
        // ex) 'click hoge' => [click, click, hoge]みたいな
        var match = key.match(delegateEventSplitter);

        // _.bindにてcallback関数のcontextをViewのcontextに書き換えた関数として生成しなおしている。
        // _.bindのがどういうものかは以下を見るのが早いかと
        // なのでeventsに設定できるcallbackは必ずViewになる
        // eventsのcallbackを別のcontextで実行する方法は見つからない。
        // ----------------------------------------------------------------------------------------------------
        // var func = function(greeting){ return greeting + ': ' + this.name };
        // func = _.bind(func, {name: 'moe'}, 'hi');
        // func();
        // => 'hi: moe'
        // ----------------------------------------------------------------------------------------------------
        this.delegate(match[1], match[2], _.bind(method, this));
      }
      return this;
    },

    // イベントの登録を行う処理
    // ViewのイベントはすべてjQueryのAPIを使って実装されている
    // イベントの登録を行う際にコンストラクタでインスタンスに対して一意なIDを採番していたが、これがここに来て意味を出した。
    // これにより、同じViewだが、インスタンスの違う場合、別のイベントとして対応することができる
    delegate: function(eventName, selector, listener) {

      // 実際にイベントを登録するときは『click.delegateEventsview1』みたいな感じになる。
      // ここでいう『delegateEventsview1』の部分はこのイベントのネームスペースとして扱えるようになり。
      //ネームスペースを利用したイベントの削除なんかができるようになるらしい。
      // https://api.jquery.com/on/
      // この記事にも書かれているが気持ち悪い挙動なので注意。
      // http://qiita.com/sasaplus1/items/0ec036c1a8789b9d9907
      this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
      return this;
    },

    // Viewに紐付けられたjQueryのイベントをすべて取っ払う
    undelegateEvents: function() {
      if (this.$el) this.$el.off('.delegateEvents' + this.cid);
      return this;
    },

    // 指定された、Viewに紐付けられたjQueryのイベントを取っ払う
    undelegate: function(eventName, selector, listener) {
      this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
      return this;
    },

    // タグ名をもとにHTMLタグの文字列を生成する。
    // window.documentのAPIをそのまま使っているだけである
    _createElement: function(tagName) {
      return document.createElement(tagName);
    },

    // this.elが定義されていた場合は、this.elを評価し、結果の文字列をView自身のDOM要素として扱うようになる。
    // this.elは文字列として定義されていてもいいし、関数として定義されていてもいい。.result便利だなぁ
    // this.elが存在しない場合はthis.attributes, this.id, this.className, this.tagNameに定義されているものを利用してView自身のDOM要素として扱うことになる
    // この中でthis.tagNameに関してはデフォルトで『div』が定義されているので、仮に何にも定義されていない状態だと、『<div>』がViewのDOMになる
    // ※ Model.attributesとView.attributesは全く世界が違うので注意すること。View.attributesはDOM要素のattributesのことを指している。『href, srcとかのあれね。』
    _ensureElement: function() {
      if (!this.el)
      {
        // this.elが定義されていない場合は、Viewのメンバ変数を利用してDOMを生成する
        var attrs = _.extend({}, _.result(this, 'attributes'));
        if (this.id) attrs.id = _.result(this, 'id');
        if (this.className) attrs['class'] = _.result(this, 'className');
        this.setElement(this._createElement(_.result(this, 'tagName')));
        this._setAttributes(attrs);
      }
      else
      {
        // this.elを定義しているときはそれを単純に利用するだけ
        this.setElement(_.result(this, 'el'));
      }
    },

    // View自身のDOM要素にattributesを設定する
    // jQueryのAPIをそのまま使っているだけである。
    _setAttributes: function(attributes) {
      this.$el.attr(attributes);
    }

  });

```

- Backbone.jsを生で扱う場合はレンダリングに関する記述はすべてユーザーが行わなければいけない。(renderを書き下ろさないといけない)
- renderは『return this;』で終わるように書くのが一般的です。
- 自身のjQueryのDOM要素に対するショートカットがチラホラ存在する
- イベント周りの実装はjQueryのAPIをそのまま使っている
- Viewを削除するときは、.removeを使ってあげないとイベント購読が終わらないので注意すること

普段、Marionette.jsを使うので、Backbone.Viewでサポートできている機能がどこまでなのかをはっきりわかったので気持ちよかったです。

# おまけ

今回はボリュームが少なかったので、Viewの振る舞いを確認するためのサンプルコードを置いて帰ります。

```html

<!DOCTYPE html>
<html>
<head>
  <meta charset='utf8'>
  <title>Backbone Sample</title>
</head>
<body>
  <div id="sample1"></div>
  <div id="sample2"></div>
  <div id="sample3"></div>
  <div id="sample4"></div>



<script src="vendor/jquery.js"></script>
<script src="vendor/underscore.js"></script>
<script src="../backbone.js"></script>
<script>

//////////////////////////////////////////////////////////
// render!は何回出力されるでしょうか
//////////////////////////////////////////////////////////
var Sample1 = Backbone.View.extend({
  template: '<p class="hoge">Hello</p>',
  el: '#sample1',
  events: {
    'click .hoge': 'onClick',
  },
  initialize: function() {
    this.listenTo(this.model, 'change', this.render);
  },
  render: function() {
    console.log('render!');
    this.$el.html(this.template);
    return this;
  },
  onClick: function()
  {
    console.log('click!');
  }
});

console.log('----------------------------------------------')
var sample1Model = new Backbone.Model({});
new Sample1({model: sample1Model});
sample1Model.set('hoge', 'Hoge');
sample1Model.set('hoge', 'Hoge');
sample1Model.set('hoge', 'Fuga', {silent: true});
console.log('----------------------------------------------')






//////////////////////////////////////////////////////////
// Fugaをclickすると何が出力されるでしょうか
//////////////////////////////////////////////////////////
var Hoge = Backbone.Model.extend({
  name: 'hoge',
  fuga: function()
  {
    console.log(this.name);
  }
});
var hoge = new Hoge();
var Sample2 = Backbone.View.extend({
  name: 'fuga',
  template: '<p class="fuga">Fuga</p>',
  el: '#sample2',
  events: {
    'click .fuga': hoge.fuga,
  },
  render: function() {
    this.$el.html(this.template);
    return this;
  },
});

console.log('----------------------------------------------')
new Sample2({model: sample1Model}).render();
console.log('----------------------------------------------')



//////////////////////////////////////////////////////////
// 以下のコードを実行すると『alert!』が1秒ごとに出力されます。
// 出力をとめつつ、ブラウザのDOMツリーからViewの要素を消し去りたいときはどうすればいいでしょうか
//////////////////////////////////////////////////////////
var Timer = Backbone.Model.extend({
  initialize: function()
  {
    var self = this;
    setInterval(function(){
      self.trigger('alert');
    }, 1000);
  }
});
var timer = new Timer();
var Sample3 = Backbone.View.extend({
  template: '<p>Piyo</p>',
  el: '#sample3',
  initialize: function()
  {
    this.listenTo(timer, 'alert', function(){
      console.log('alert!');
    })
  },
  render: function() {
    this.$el.html(this.template);
    return this;
  },
});

console.log('----------------------------------------------')
var view = new Sample3().render();

// それぞれ、A,B,C,Dがどのような振る舞いになるかをお応えください。
// イベントはとまるか止まらないか・DOMツリーから要素は消えるかどうか
// A: view.$el.remove();
// B: view.remove();
// C: view = null;
// D: $(view.el).remove();

// timerのイベント登録状況はこれで詳しく確認できる
console.log(timer._events);
console.log('----------------------------------------------')

</script>
</html>

```