Object.observe() によるデータ バインディングの変革

Addy Osmani 氏
Addy Osmani 氏

はじめに

革命が起ころうとしている。JavaScript に新たに加わったことで、データ バインディングについて知っているあらゆるものが変わります。また、モデルの編集や更新を監視する MVC ライブラリの数も変化します。物件の観察が必要なアプリのパフォーマンスを大幅に向上させる準備はできましたか?

Object.observe()Chrome 36 Stable 版がリリースされました。[WOOOO. THE CROWD GOES WILD] です。

Object.observe() は将来の ECMAScript 標準の一部であり、JavaScript オブジェクトに対する変更を非同期に監視するためのメソッドです。別のライブラリは必要ありません。オブザーバーは、一連のモニタリング対象オブジェクトに加えられた一連の変更を記述する、時間順に並んだ一連の変更レコードを受け取ることができます。

// Let's say we have a model with data
var model = {};

// Which we then observe
Object.observe(model, function(changes){

    // This asynchronous callback runs
    changes.forEach(function(change) {

        // Letting us know what changed
        console.log(change.type, change.name, change.oldValue);
    });

});

変更が加えられるたびに、次のように報告されます。

変更を報告しました。

Object.observe()(私は O.o() または Oooooooo と呼んでいます)を使用すると、フレームワークを必要とせずに双方向のデータ バインディングを実装できます。

使用すべきでないというわけではありません。複雑なビジネス ロジックを持つ大規模なプロジェクトでは、独自のフレームワークは非常に貴重であるため、使用を継続する必要があります。新しい開発者の志向がシンプルになり、コードのメンテナンスが少なくて済むようになり、一般的なタスクを実行する方法にパターンが課せられます。不要な場合は、Polymer(すでに O.o() を活用している)など、より小さく、焦点を絞ったライブラリを使用できます。

フレームワークや MV* ライブラリを大量に使用する場合であっても、O.o() は、同じ API を維持したまま、より速く、より簡単な実装で、健全なパフォーマンス向上をもたらす可能性があります。たとえば、昨年の Angular による調査では、モデルに変更を加えるベンチマークでは、ダーティチェックに 1 更新あたり 40 ミリ秒、O.o() に 1 ~ 2 ミリ秒かかった(20 ~ 40 倍改善)ことがわかりました。

大量の複雑なコードを必要としないデータ バインディングでは、変更をポーリングする必要もなくなるため、バッテリー駆動時間が長くなります。

O.o() ですでに販売している場合は、機能の紹介にスキップするか、それによって解決される問題について先にお読みください。

何を観察したいのか?

データの観察とは、通常、特定の種類の変更に注意を払うことを意味します。

  • 未加工の JavaScript オブジェクトに対する変更
  • プロパティが追加、変更、削除されるタイミング
  • 配列の要素がスプライスされた(スプライシングされた)場合
  • オブジェクトのプロトタイプに対する変更

データ バインディングの重要性

モデルビュー コントロールの分離を考慮すると、データ バインディングが重要になります。HTML は優れた宣言型メカニズムですが、完全に静的です。データと DOM の関係を宣言し、DOM を最新の状態に保つのが理想的です。これにより、アプリケーションの内部状態やサーバー間で DOM との間でデータを送信するだけの、非常に反復的なコードを作成する手間が省け、多くの時間を節約できます。

データ バインディングは、複雑なユーザー インターフェースがあり、データモデルの複数のプロパティとビューの複数の要素の関係を接続する必要がある場合に特に便利です。これは、現在開発しているシングルページ アプリケーションではごく一般的です。

Google は、ブラウザ内のデータをネイティブに監視する方法を考案することで、現在広く使用されている時間のかかるハックに頼ることなく、モデルデータの変更を JavaScript フレームワーク(およびユーザーが作成する小さなユーティリティ ライブラリ)に監視させる方法を提供しています。

現在の状況

ダーティ チェック

データ バインディングをどこで見たことがありますか。ウェブアプリの構築に最新の MV* ライブラリ(Angular、Knockout など)を使用する場合は、モデルデータを DOM にバインドすることに慣れているでしょう。復習として、以下は電話リストアプリの例です。ここでは、phones 配列(JavaScript で定義)内の各電話番号の値をリストアイテムにバインドして、データと UI が常に同期されるようにしています。

<html ng-app>
  <head>
    ...
    <script src='angular.js'></script>
    <script src='controller.js'></script>
  </head>
  <body ng-controller='PhoneListCtrl'>
    <ul>
      <li ng-repeat='phone in phones'>
        
        <p></p>
      </li>
    </ul>
  </body>
</html>

コントローラの JavaScript:

var phonecatApp = angular.module('phonecatApp', []);

phonecatApp.controller('PhoneListCtrl', function($scope) {
  $scope.phones = [
    {'name': 'Nexus S',
     'snippet': 'Fast just got faster with Nexus S.'},
    {'name': 'Motorola XOOM with Wi-Fi',
     'snippet': 'The Next, Next Generation tablet.'},
    {'name': 'MOTOROLA XOOM',
     'snippet': 'The Next, Next Generation tablet.'}
  ];
});

基になるモデルデータが変更されるたびに、DOM 内のリストが更新されます。Angular はどのようにこれを実現しているのでしょうか。舞台裏では、ダーティチェックと呼ばれる処理が行われています。

ダーティ チェック

ダーティチェックの基本的な考え方は、データが変更される可能性があるため、ライブラリはダイジェストまたは変更サイクルを介して、変更されたかどうかを確認する必要があります。Angular の場合、変更の有無を確認するために監視対象に登録されているすべての式がダイジェスト サイクルによって識別されます。モデルの以前の値を把握し、値が変更された場合は変更イベントが発生します。デベロッパーにとっての主なメリットは、使い勝手がよく、きれいに構成できる未加工の JavaScript オブジェクト データを利用できることです。この方法の欠点は、アルゴリズムの動作が不適切で、非常に高コストになる可能性があることです。

ダーティ チェック。

このオペレーションの費用は、観測されたオブジェクトの総数に比例します。ダーティ チェックをたくさんする必要があるかもしれません。また、データが変更された可能性がある場合にダーティチェックをトリガーする方法が必要になる場合もあります。そのためにフレームワークで使用される巧妙な手法は数多くあります。これが完璧になるかどうかは不明です

ウェブ エコシステムには、独自の宣言型メカニズムのイノベーションと進化を可能にする能力が必要です。たとえば、

  • 制約ベースのモデルシステム
  • 自動永続システム(IndexedDB や localStorage の変更の永続化など)
  • コンテナ オブジェクト(Ember、Backbone)

コンテナ オブジェクトは、フレームワークが内部にデータを保持するオブジェクトを作成する場所です。データへのアクセス権を持ち、設定または取得したものをキャプチャして、内部でブロードキャストできます。うまくいきます比較的パフォーマンスが高く、アルゴリズムの動作も優れています。Ember を使用したコンテナ オブジェクトの例を次に示します。

// Container objects
MyApp.president = Ember.Object.create({
  name: "Barack Obama"
});
 
MyApp.country = Ember.Object.create({
  // ending a property with "Binding" tells Ember to
  // create a binding to the presidentName property
  presidentNameBinding: "MyApp.president.name"
});
 
// Later, after Ember has resolved bindings
MyApp.country.get("presidentName");
// "Barack Obama"
 
// Data from the server needs to be converted
// Composes poorly with existing code

ここで何が変わったのかを発見する費用は、変わった要素の数に比例します。もう一つの問題は、この種のオブジェクトを使用していることです。一般的には、サーバーから取得したデータを、これらのオブジェクトを監視できるように変換する必要があります。

ほとんどのコードは元データに対する処理が可能であることを前提としているため、既存の JS コードでのコンポーズは特に改善されません。このような特殊なタイプのオブジェクトには適していません。

Introducing Object.observe()

理想的には、両方の長所を兼ね備えていることが理想的です。つまり、常にダーティチェックを行わずに、元データ オブジェクト(通常の JavaScript オブジェクト)をサポートしてデータをモニタリングする方法です。アルゴリズムの動作に問題がない。適切に構成され、プラットフォームに組み込まれているもの。これが Object.observe() がもたらす優れた点です。

これにより、オブジェクトの監視、プロパティの変更、変更内容の変更レポートの確認が可能になります。理論の説明は以上です。次にコードを見てみましょう。

Object.observe()

Object.observe() と Object.unobserve()

モデルを表す単純な標準の JavaScript オブジェクトがあるとします。

// A model can be a simple vanilla object
var todoModel = {
  label: 'Default',
  completed: false
};

これで、オブジェクトにミューテーション(変更)が行われるたびに呼び出されるコールバックを指定できます。

function observer(changes){
  changes.forEach(function(change, i){
      console.log('what property changed? ' + change.name);
      console.log('how did it change? ' + change.type);
      console.log('whats the current value? ' + change.object[change.name]);
      console.log(change); // all changes
  });
}

次に、O.o() を使用してこれらの変更を監視できます。1 つ目の引数としてオブジェクトを渡し、2 つ目の引数としてコールバックを渡します。

Object.observe(todoModel, observer);

まず、Todos モデル オブジェクトに変更を加えます。

todoModel.label = 'Buy some more milk';

コンソールを確認すると、有用な情報が表示されています。どのプロパティがどのように変更されたか、どのように変更されたか、新しい値が何であるかを把握しています。

コンソール レポート

ボーナス さようなら、ダーティチェック!あなたの墓石は Comic Sans に彫り込んでください。別のプロパティも変更します。今回は completeBy:

todoModel.completeBy = '01/01/2014';

ご覧のように、変更レポートが再度正常に取得されています。

レポートを変更。

これでここで、オブジェクトから「completed」プロパティを削除するとどうなるでしょうか。

delete todoModel.completed;
完了

ご覧のように、返された変更のレポートには削除に関する情報が含まれています。想定どおり、プロパティの新しい値が未定義になりました。これで、プロパティが追加されたときに通知できるようになりました。いつ削除されたか。基本的には、オブジェクトのプロパティの一連の(「new」、「deleted」、「reconfiguration」)と、プロトタイプが変更されます(proto)。

他のモニタリング システムと同様に、変更のリッスンを停止する方法も存在します。この場合は Object.unobserve() です。O.o() と同じシグネチャを持ちますが、次のように呼び出すことができます。

Object.unobserve(todoModel, observer);

以下に示すように、これの実行後にオブジェクトに加えられたミューテーションでは、変更レコードのリストが返されなくなりました。

ミューテーション

対象の変更の指定

ここまで、モニタリング対象オブジェクトの変更の一覧を取得する基本的な方法を見てきました。オブジェクトに対して行われたすべての変更ではなく、一部の変更のみを取得したい場合はどうすればよいでしょうか。すべての人に迷惑メールフィルタが必要です。オブザーバーは、確認したい変更だけを承認リストで指定できます。これは、次のように O.o() の 3 番目の引数を使用して指定できます。

Object.observe(obj, callback, optAcceptList)

この使用例を見ていきましょう。

// Like earlier, a model can be a simple vanilla object

var todoModel = {
  label: 'Default',
  completed: false

};


// We then specify a callback for whenever mutations 
// are made to the object
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })

};

// Which we then observe, specifying an array of change 
// types we're interested in

Object.observe(todoModel, observer, ['delete']);

// without this third option, the change types provided 
// default to intrinsic types

todoModel.label = 'Buy some milk'; 

// note that no changes were reported

ここでラベルを削除すると、次の種類の変更が報告されます。

delete todoModel.label;

受け入れ型のリストを O.o() に指定しない場合は、デフォルトで「本質的」なオブジェクト変更タイプ(addupdatedeletereconfigurepreventExtensions(拡張不能になったオブジェクトを監視できない場合))になります。

通知

O.o() には通知の概念も含まれています。スマートフォンに搭載されているわずらわしい機能とは違うものの、むしろ便利な機能です。通知はミューテーション オブザーバーに似ています。これらはマイクロタスクの最後に行われます。ブラウザのコンテキストでは、これはほとんどの場合、現在のイベント ハンドラの最後に存在します。

一般的に 1 つの作業単位が終了し、オブザーバーが作業を開始するため、タイミングが良くなります。これは優れたターンベースの処理モデルです。

Notifier を使用するワークフローは次のようになります。

通知

オブジェクトのプロパティが取得または設定されたときのカスタム通知を定義するために、実際に Notifier がどのように使用されるかの例を見てみましょう。以下のコメントにご注目ください。

// Define a simple model
var model = {
    a: {}
};

// And a separate variable we'll be using for our model's 
// getter in just a moment
var _b = 2;

// Define a new property 'b' under 'a' with a custom
// getter and setter

Object.defineProperty(model.a, 'b', {
    get: function () {
        return _b;
    },
    set: function (b) {

        // Whenever 'b' is set on the model
        // notify the world about a specific type
        // of change being made. This gives you a huge
        // amount of control over notifications
        Object.getNotifier(this).notify({
            type: 'update',
            name: 'b',
            oldValue: _b
        });

        // Let's also log out the value anytime it gets
        // set for kicks
        console.log('set', b);

        _b = b;
    }
});

// Set up our observer
function observer(changes) {
    changes.forEach(function (change, i) {
        console.log(change);
    })
}

// Begin observing model.a for changes
Object.observe(model.a, observer);
Notifications コンソール

ここでは、データ プロパティの値が変更されたときに報告します(「update」)。オブジェクトの実装でレポート対象として選択されたその他のすべて(notifier.notifyChange())。

ウェブ プラットフォームでの長年の経験から、同期型のアプローチが最初に試すべきであることがわかりました。同期型のアプローチは、頭を悩ませるのが最も簡単だからです。問題は、根本的に危険な処理モデルを生み出すことです。コードを記述しているとき、オブジェクトのプロパティを更新する場合、そのオブジェクトのプロパティを更新するという状況では、任意のコードを招待して必要な処理を実行させることは望ましくありません。関数の途中で仮定を無効化することは理想的ではありません。

オブザーバーは、誰かの途中でも呼び出されないようにするのが理想的です。一貫性のない世界で仕事をするよう求められるのは望ましくありません。より多くのエラーチェックを行うことになります。より多くの悪い状況を許容しようとします。一般的には、扱いにくいモデルです。非同期は処理が難しくなりますが、結局のところは優れたモデルです。

この問題の解決策が、合成変更レコードです。

合成変更レコード

基本的に、アクセサまたは計算済みプロパティが必要な場合は、これらの値が変更されたときに通知する必要があります。多少の手間はかかりますが、これはこのメカニズムの優れた機能として設計されており、これらの通知は、基盤となるデータ オブジェクトからの残りの通知とともに配信されます。データ プロパティから取得。

合成変更レコード

アクセサーと計算されたプロパティの監視は、notifier.notify(O.o() の別の部分)を使用して解決できます。ほとんどの観測システムでは、なんらかの形で派生値の監視が必要になります。これにはさまざまな方法があります。O.o は「正しい」方法について判断しません。計算済みプロパティは、内部(非公開)状態が変更されたときに通知するアクセサにする必要があります。notify

繰り返しになりますが、ウェブ開発者は、ライブラリによって計算済みプロパティへの通知やさまざまなアプローチが簡単になる(そしてボイラープレートを減らすことができる)ことを期待する必要があります。

次の例はサークルクラスです。ここでは、この円と radius プロパティを使用します。この場合、radius はアクセサーであり、値が変更されると、値が変更されたことを実際に通知します。これは、このオブジェクトや他のオブジェクトに対する他のすべての変更とともに配信されます。基本的に、オブジェクトを実装する場合は、合成または計算されたプロパティを必要とするか、それがどのように機能するかについての戦略を選択する必要があります。これを行うと、システム全体に適合します。

DevTools でこの動作を確認するには、コードをスキップしてください。

function Circle(r) {
  var radius = r;
 
  var notifier = Object.getNotifier(this);
  function notifyAreaAndRadius(radius) {
    notifier.notify({
      type: 'update',
      name: 'radius',
      oldValue: radius
    })
    notifier.notify({
      type: 'update',
      name: 'area',
      oldValue: Math.pow(radius * Math.PI, 2)
    });
  }
 
  Object.defineProperty(this, 'radius', {
    get: function() {
      return radius;
    },
    set: function(r) {
      if (radius === r)
        return;
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
 
  Object.defineProperty(this, 'area', {
    get: function() {
      return Math.pow(radius, 2) * Math.PI;
    },
    set: function(a) {
      r = Math.sqrt(a/Math.PI);
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
}
 
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })
}
合成変更レコード コンソール

アクセサーのプロパティ

アクセサのプロパティについて簡単に説明します。前述のように、データ プロパティで観測できるのは値の変更のみです。計算済みプロパティやアクセサ用ではありません。これは、JavaScript にはアクセサの値を変更するという概念がないからです。アクセサーとは関数のコレクションです。

アクセサに割り当てると、JavaScript はそこで関数を呼び出すだけで、視点からは何も変わりません。なんらかのコードを実行する機会が与えられるだけです。

問題は意味的に、上記の値への - 5 への割り当てを見ることができることです。ここで何が起きたかがわかる必要があります。これは実際には解決できない問題ですこの例はその理由を示しています。これは任意のコードになり得るため、どのシステムもこれが意味することを知る方法はありません。この場合は、任意の処理を実行できます。アクセスするたびに値が更新されるため、変更したかどうかを尋ねるのはあまり意味がありません。

1 回のコールバックで複数のオブジェクトを監視する

O.o() で可能なもう 1 つのパターンは、単一のコールバック オブザーバーの概念です。これにより、1 つのコールバックをさまざまなオブジェクトの「オブザーバー」として使用できます。コールバックは、「マイクロタスクの最後」に観測したすべてのオブジェクトに対する変更の完全なセットを配信します(Mutation Observer と同様です)。

1 回のコールバックで複数のオブジェクトを監視する

大規模な変更

非常に大規模なアプリに取り組んでいて、定期的に大規模な変更に取り組む必要があるかもしれません。オブジェクトで、多くのプロパティに影響を与える大規模なセマンティックな変更を(大量のプロパティ変更をブロードキャストするのではなく)より簡潔な方法で記述したい場合があります。

O.o() は、すでに導入した notifier.performChange()notifier.notify() という 2 つのユーティリティという形でこれに役立ちます。

大規模な変更

大規模な変更を記述する例を見てみましょう。ここでは、いくつかの数学ユーティリティ(multiply、increment、incrementAndMultiply)を使用して Thingy オブジェクトを定義します。ユーティリティが使用されるたびに、作業のコレクションが特定の種類の変更で構成されていることをシステムに通知します。

例: notifier.performChange('foo', performFooChangeFn);

function Thingy(a, b, c) {
  this.a = a;
  this.b = b;
}

Thingy.MULTIPLY = 'multiply';
Thingy.INCREMENT = 'increment';
Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply';


Thingy.prototype = {
  increment: function(amount) {
    var notifier = Object.getNotifier(this);

    // Tell the system that a collection of work comprises 
    // a given changeType. e.g
    // notifier.performChange('foo', performFooChangeFn);
    // notifier.notify('foo', 'fooChangeRecord');
    notifier.performChange(Thingy.INCREMENT, function() {
      this.a += amount;
      this.b += amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT,
      incremented: amount
    });
  },

  multiply: function(amount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.MULTIPLY, function() {
      this.a *= amount;
      this.b *= amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.MULTIPLY,
      multiplied: amount
    });
  },

  incrementAndMultiply: function(incAmount, multAmount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() {
      this.increment(incAmount);
      this.multiply(multAmount);
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT_AND_MULTIPLY,
      incremented: incAmount,
      multiplied: multAmount
    });
  }
}

次に、オブジェクトに 2 つのオブザーバーを定義します。1 つは変更のキャッチオールで、もう 1 つは定義した特定の受け入れタイプ(Thingy.INCREMENT、Thingy.MULTIPLY、Thingy.INCREMENT_AND_MULTIPLY)についてのみレポートします。

var observer, observer2 = {
    records: undefined,
    callbackCount: 0,
    reset: function() {
      this.records = undefined;
      this.callbackCount = 0;
    },
};

observer.callback = function(r) {
    console.log(r);
    observer.records = r;
    observer.callbackCount++;
};

observer2.callback = function(r){
    console.log('Observer 2', r);
}


Thingy.observe = function(thingy, callback) {
  // Object.observe(obj, callback, optAcceptList)
  Object.observe(thingy, callback, [Thingy.INCREMENT,
                                    Thingy.MULTIPLY,
                                    Thingy.INCREMENT_AND_MULTIPLY,
                                    'update']);
}

Thingy.unobserve = function(thingy, callback) {
  Object.unobserve(thingy);
}

では、このコードを使ってゲームを始めましょう。新しい Thingy を定義しましょう。

var thingy = new Thingy(2, 4);

確認してから変更を加えます。すごい、楽しい。いろいろあります。

// Observe thingy
Object.observe(thingy, observer.callback);
Thingy.observe(thingy, observer2.callback);

// Play with the methods thingy exposes
thingy.increment(3);               // { a: 5, b: 7 }
thingy.b++;                        // { a: 5, b: 8 }
thingy.multiply(2);                // { a: 10, b: 16 }
thingy.a++;                        // { a: 11, b: 16 }
thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }
大規模な変更

「パフォーマンスを行う機能」内のすべての作業は、「大きな変更」の作業とみなされます。「大きな変更」を受け入れるオブザーバーは、「大きな変更」のレコードのみを受け取ります。そうでないオブザーバーは、「関数を実行」した作業の結果として生じる根本的な変更を受け取ります。

配列の観察

オブジェクトの変更の監視については長らく説明しましたが、配列についてはどうでしょうか。いい質問ですね。誰かが「いい質問だ」と言ったときこんな素晴らしい質問をしてくれておめでとうって忙しいんだけど、答えが出てこないんだけど。配列を操作するための新しいメソッドもあります。

Array.observe() は、スプライスやシフト解除など、自身に対する大規模な変更(暗黙的に長さを変更するもの)を「スプライス」変更レコードとして処理するメソッドです。内部的には notifier.performChange("splice",...) を使用します。

次の例では、モデルの「配列」を確認し、基になるデータが変更されたときに、同様に変更のリストを取得しています。

var model = ['Buy some milk', 'Learn to code', 'Wear some plaid'];
var count = 0;

Array.observe(model, function(changeRecords) {
  count++;
  console.log('Array observe', changeRecords, count);
});

model[0] = 'Teach Paul Lewis to code';
model[1] = 'Channel your inner Paul Irish';
配列の観察

パフォーマンス

O.o() の計算パフォーマンスへの影響は、読み取りキャッシュのようなものと考えることができます。一般的に、キャッシュは次のような場合におすすめです(重要度の高い順)。

  1. 読み取りの頻度が書き込みの頻度の大半を占めます。
  2. キャッシュを作成することで、書き込み時に発生する一定の作業量と引き換えに、読み取り時のパフォーマンスをアルゴリズム的に向上させることができます。
  3. 書き込み速度が一定時間遅くなることは許容できる。

O.o() は 1)のようなユースケース向けに設計されています。

ダーティチェックでは、監視対象のすべてのデータのコピーを保持する必要があります。つまり、ダーティチェックでは、O.o() では得られない構造的なメモリコストが発生します。ダーティチェックは、適切なストップギャップ ソリューションではありますが、根本的に漏れのある抽象化でもあり、アプリケーションにとって不必要な複雑さを生み出す可能性があります。

その理由は、ダーティチェックは、データが変更される可能性があるたびに実行する必要があります。これを行うための非常に堅牢な方法はなく、どのようなアプローチにも大きな欠点があります(たとえば、ポーリング間隔をチェックすると、視覚的なアーティファクトやコード間の競合状態が発生するリスクがあります)。また、ダーティチェックにはオブザーバーのグローバル レジストリも必要となり、メモリリークの危険が生じ、O.o() が回避するティアダウン コストが発生します。

いくつかの数字を見てみましょう。

以下のベンチマーク テスト(GitHub で入手可能)では、ダーティチェックと O.o() を比較できます。これらは、Observable-Object-Set-Size と Number-Of-Mutations のグラフとして構成されています。一般的な結果では、ダーティ チェックのパフォーマンスは観測されたオブジェクトの数にアルゴリズム的に比例し、O.o() のパフォーマンスは行われたミューテーションの数に比例します。

ダーティ チェック

ダーティ チェックのパフォーマンス

Object.observe() をオンにした Chrome

パフォーマンスを観察する

Object.observe() のポリフィル

O.o() は Chrome 36 で使用できますが、他のブラウザではどうでしょうか?Google がお手伝いしますのでご安心ください。Polymer の Observe-JS は、O.o() のポリフィルです。O.o() が存在する場合はネイティブ実装を使用しますが、それ以外の場合はポリフィルし、上部に便利なシュガーリングを追加します。変化を総括する全体像を提供し、何が変化したかについてのレポートを提供します。次の 2 つの非常に強力な機能が公開されています。

  1. 経路を監視できます。つまり、特定のオブジェクトから「foo.bar.baz」を監視すると、そのパスの値が変更されたときに通知されます。パスにアクセスできない場合、値は未定義と見なされます。

以下は、指定されたオブジェクトのパスにある値を監視する例です。

var obj = { foo: { bar: 'baz' } };

var observer = new PathObserver(obj, 'foo.bar');
observer.open(function(newValue, oldValue) {
  // respond to obj.foo.bar having changed value.
});
  1. 配列スプライスについて説明してくれます。配列スプライスは基本的に、配列の旧バージョンを新バージョンに変換するために配列に対して実行する必要があるスプライス オペレーションの最小セットです。これは一種の変換または配列の別のビューです。これは、以前の状態から新しい状態に移行するために必要な最小限の作業量です。

配列の変更を最小スプライスのセットとしてレポートする例:

var arr = [0, 1, 2, 4];

var observer = new ArrayObserver(arr);
observer.open(function(splices) {
  // respond to changes to the elements of arr.
  splices.forEach(function(splice) {
    splice.index; // index position that the change occurred.
    splice.removed; // an array of values representing the sequence of elements which were removed
    splice.addedCount; // the number of elements which were inserted.
  });
});

フレームワークと Object.observe()

前述のように、O.o() はフレームワークとライブラリに、この機能をサポートするブラウザでデータ バインディングのパフォーマンスを向上させる大きな機会を提供します。

Ember の Yehuda Katz 氏と Erik Bryn 氏は、O.o() のサポートを追加することが Ember の短期的なロードマップであることを認めています。Angular の Misko Hervy 氏は、Angular 2.0 で改善された変更検出についての設計書を執筆しています。長期的なアプローチは、Chrome 安定版になったら Object.observe() を利用し、それまでは独自の変更検出アプローチである Watchtower.js を選択することです。すごいわ。わくわくする。

まとめ

O.o() はウェブ プラットフォームの強力な追加機能であり、すぐに使用できます。

いずれは、この機能がより多くのブラウザで実装され、JavaScript フレームワークがネイティブのオブジェクト監視機能にアクセスしてパフォーマンスを向上できるようになることを期待しています。Chrome をターゲットとする場合は、Chrome 36(以降)で O.o() を使用でき、この機能は今後の Opera リリースでも利用できるようになります。

ですから、JavaScript フレームワークの作成者に、Object.observe()アプリのデータ バインディングのパフォーマンスを改善するためにどのように JavaScript フレームワークを使用する予定かについて説明を求めましょう。今後も、間違いなくエキサイティングな時期が訪れるでしょう。

リソース

コメントとレビューに協力してくれた Rafael Weinstein、Jake Archibald、Eric Bidelman、Paul Kinlan、Vivian Cromwell に感謝します。