ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

次世代JavaScriptでデータバインディング: Object.observe() を試す

1. はじめに、

本記事は、HTML5 Advent Calendar 2012の参加(6日目)エントリーです。
当初は昨年のアドベントカレンダーでテーマにしたマイナーAPIをネタにして書こうかと考えていたのですが、探してもあまりピンとくるものがなく、いつものごとく新技術ネタに飛びついてしまう習性がでてしまったので今回次世代JavaScript(ES.harmony)ネタ(Object.observe)を書かせていただきます。
現時点では直接HTML5とは関係ありませんが、標準で利用できるようになったら皆さんがお世話になる機会が必ず増えると予感しています。今の時点で知っておいてもらっても絶対損はないと思いますのでどうかご了承ください。 (_O_)

2. Object.observe() とは何か?

先日のHTML5勉強会でも取り上げられましたが、最近 JavaScriptMVC フレームワークが大流行です。publickey さんの記事「JavaScript MVCフレームワークはすでに十種類以上、その比較や最新情報などのまとめ」を見てみるとその状況がよくわかるでしょう。

で、このMVCフレームワークを実装する際に必要なのがデータバインディングの技術です。モデルとビューをきっちり分けるには、このデータバインディングをきっちり作ることが重要と言われています。しかしMVCフレームワークの作者はこれまで"dirty check"と呼ばれる結構泥臭い実装をして苦労していたようです。そこに業を煮やしたGoogleさんが、このデータバインディングの実装をすっきりできるよう Object.observe() という新しいAPIをなんと ECMAScript に実装してしまおうと、今年の初めに提案を行いました。(さすがV8を開発していると強いです。)

この Object.observe はざっと言うと、「任意のオブジェクトプロパティの操作をObserveしてコールバックを実行することができる」API です。
例えて言うなら、
「任意のオブジェクトに対してプロパティ操作のイベントハンドラーを登録できる。」
ということでしょうか。(プロパティの操作をイベントと呼んだら本当は間違いなのかもしれませんが、便利なのでここでは使います。)
ちなみにFirefox ではDOMを対象とした類似の機能 MutationObserver が実装されているようです。

現在議論は進み、 Object.observer は strawman proposal で承認され、 ES.harmony の候補として仕様化の議論がされている最中です。そして既にV8で試験実装も行われ、先月末よりChrome Canary や dev channel で実際に使うことができるようになっています。さらにAngular.jsにObject.observeを実装したブランチもできています。

そこで今回、この Object.observe の機能の紹介とそれを使った簡単な機能デモをしたいと思います。

現在 ES.harmony で議論されている observe のAPI は、

  • Object.observe (オブジェクトへのコールバックの登録)
  • Object.unobserve (オブジェクトへのコールバックの登録解除)
  • Object.deliverChangeRecords (コールバック実行を進めて値を変える)
  • Object.getNotifier (Notifierオブジェクトを取得する)

の4つです。 そして標準でObserve(検知)できるプロパティ操作は、

  • new (プロパティの新規作成)
  • updated (プロパティ値の更新)
  • reconfigured (プロパティ設定の変更)
  • deleted (プロパティの削除)

の4種類用意されています。

まずは単純な Object.observe() を使った例を示します。

注意:デモを利用する注意事項

現在(2012年12月6日時点)Object.observe() を利用するには、 Chrome Canary か Chrome Dev Channel(25.0.1337.0 dev-m以降)を利用する必要があります。また chrome://flags/でJavaScript の試験運用機能を有効する」が有効になっていないといけません。設定項目は、下図を参照して下さい。(試験運用機能の有効化に伴う警告・注意事項をよく読んでからお試しください)

3. まずは簡単なサンプルを試す

Object.observe() はオブジェクトにコールバックを登録するAPIです。第一引数に observe(検知)するオブジェクト、第二日引数にコールバック関数を取ります。

Object.observe(obj, callback)

コールバックの引数には、各オブジェクトプロパティの操作で生成されたChangeRecordオブジェクトの配列リストが渡されます。
ChangeRecordオブジェクトは、(type, name, oldValue, object)の4つのプロパティを持ち、それぞれ下記のような情報を保持しています。

function callback(changes) {
  changes.forEach(function(change) {
      console.log(change.type);     // change種類 new/updated/reconfigured/delted
      console.log(change.name);     // プロパティ名
      console.log(change.oldValue); // 以前の値
      console.log(change.object);   // 監視しているオブジェクト
  });
}

では実際に試してみましょう。
オブジェクトを observe して4つのパターンのコールバックを発生させてやります。

  var obj = {a: 1};

  Object.observe(obj, output); // コールバックの登録

  obj.b = 2; // (1)プロパティ新規作成
  obj.a = 2; // (2)プロパティ更新
  Object.defineProperties(obj, {a: { enumerable: false}}); // (3)プロパティ設定変更
  delete obj.b; // (4)プロパティ削除
  
   function output(change) {
      // コールバック関数。ここにDOMへの出力操作が入る。
    }

実際のコードはこちら。https://gist.github.com/4220037
デモ1のページ(要 Chrome Canary/Dev Channel)http://html5.ohtsu.org/html5advent2012/observer1.html

出力は以下のようになります。

ちゃんと4つのオブジェクトプロパティ操作のイベントが捉えられ、コールバック関数(output)が実行されてDOMが書き換わっています。

4. 自分好みの Notify を作る。

observer で検知するイベントは4つの種類だけでなく、なんと独自でイベントのカスタム化ができます
Notifier というオブジェクトを使うと accessor property (getter/setterを使ったプロパティ)に対してカスタムイベントのひも付が可能です。そのためには、Object.getNotifier() を使って Notifier オブジェクトを取得し、nifity メソッドで ChangeEvent のプロパティを設定する必要があります。
以下に time_updated と time_read の2つの Notify を追加したサンプルコードを示します。

  var obj2 = {_time: new Date(0)};
  var notifier = Object.getNotifier(obj2); // Notifierの取得
  Object.defineProperties(obj2, { // assessor property の設定 
    _time: {
      enumerable: false,
      configrable: false
    },
    seen: {
      set: function(val) {
        var notifier = Object.getNotifier(this);
        notifier.notify({
          type: 'time_updated', // 時間更新イベントの定義
          name: 'seen',
          oldValue: this._time
        });
        this._time = val;
      },
      get: function() {
        var notifier = Object.getNotifier(this);
        notifier.notify({
          type: 'time_read', // 時間参照イベントの定義
          name: 'seen',
          oldValue: this._time
        });
        return this._time;
      }
    }
  });

  Object.observe(obj2, output); // コールバックの登録

そして以下のプロパティ操作を行います。

  var first_time = obj2.seen;  // 時間参照イベント
  obj2.seen = new Date();      // 時間更新イベント
  var second_time = obj2.seen; // 時間参照イベント

実際のコードはこちら。https://gist.github.com/4220059
デモ2のページ(要 Chrome Canary/Dev Channel)http://html5.ohtsu.org/html5advent2012/observer2.html

出力は以下の通りになります

おぉ、これでtime_updated, time_read の独自タイプのイベントが取得できていることがわかります。

5. コールバックの実行タイミングの制御する。

通常 Object.observe のコールバックは、すべての JavaScriptの実行が終わった一番最後に起動されます。(Node.js の process.nextTick() をご存じの方は、それと同じタイミングです。)
そのため同じプロパティに複数操作を加えると、コールバック時に参照できるのは一番最後の操作後の値です。先のデモを改良して以下のように Observe されているオブジェクトのプロパティを異なった値で7回更新します。(コールバック実行ループを避けるため time_read の Notification は削除しています)

  obj3.seen = new Date(2013, 0, 1, 0, 0, 0); // 時間更新イベント
  obj3.seen = new Date(2013, 0, 2, 0, 0, 0); // 時間更新イベント
  obj3.seen = new Date(2013, 0, 3, 0, 0, 0); // 時間更新イベント
  obj3.seen = new Date(2013, 0, 4, 0, 0, 0); // 時間更新イベント
  obj3.seen = new Date(2013, 0, 5, 0, 0, 0); // 時間更新イベント
  obj3.seen = new Date(2013, 0, 6, 0, 0, 0); // 時間更新イベント
  obj3.seen = new Date(2013, 0, 7, 0, 0, 0); // 時間更新イベント

実際のコードはこちら。https://gist.github.com/4220068
デモ3のページ(要 Chrome Canary/Dev Channel)http://html5.ohtsu.org/html5advent2012/observer3.html

出力は以下の通りになり、コールバック中でプロパティ値を参照するとみんな同じ値(2013/1/7)になってしまいます。

こういった状況だと困る場合もあるでしょう。こんな時のために強制的にコールバックを実行して値を変えさせるAPIが用意されています。 Object.deliverChangeRecords()です。

以下のコードのようにプロパティ値更新後、毎回 Object.deliverChangeRecords() によってコールバックの実行ターンを更新してやります。

  obj4.seen = new Date(2013, 0, 1, 0, 0, 0); // 時間更新イベント
  Object.deliverChangeRecords(output);       // 次のターン
  obj4.seen = new Date(2013, 0, 2, 0, 0, 0); // 時間更新イベント
  Object.deliverChangeRecords(output);       // 次のターン
  obj4.seen = new Date(2013, 0, 3, 0, 0, 0); // 時間更新イベント
  Object.deliverChangeRecords(output);       // 次のターン
  obj4.seen = new Date(2013, 0, 4, 0, 0, 0); // 時間更新イベント
  Object.deliverChangeRecords(output);       // 次のターン
  obj4.seen = new Date(2013, 0, 5, 0, 0, 0); // 時間更新イベント
  Object.deliverChangeRecords(output);       // 次のターン
  obj4.seen = new Date(2013, 0, 6, 0, 0, 0); // 時間更新イベント
  Object.deliverChangeRecords(output);       // 次のターン
  obj4.seen = new Date(2013, 0, 7, 0, 0, 0); // 時間更新イベント

実際のコードはこちら。https://gist.github.com/4220075
デモ4のページ(要 Chrome Canary/Dev Channel)http://html5.ohtsu.org/html5advent2012/observer4.html

そうするとObject.deliverChangeRecords()のタイミングで毎回コールバック実行が行われ、値が更新されているのがわかります。

5. まとめ

このように実際使ってみると非常に強力な機能で、通常でもこのAPIを使っうとロジックと操作をうまく分離したコードが書けるのではないかと感じます。 MVC フレームワーク内だけで使われるのはもったいないです。
今後 ES.next の候補として取り入れられるかどうかはわかりませんが、将来プログラミングの幅を広げる Object.observe() の機能は、今のうちから知っておいても損はないと思います。