blog

Emberで非同期を扱う

バインディングやコンピューテッドプロパティなど、多くのEmberのコンセプトは非同期動作を扱うように設計されています。...

Jul 27, 2016 · 10 min. read
シェア

バインディングや計算されたプロパティなど、多くのEmberのコンセプトは非同期動作を実現するように設計されています。

琥珀なし

まず、jQueryやイベントベースのMVCフレームワークを使用して、非同期動作を管理する方法を見てみましょう。

Webアプリケーションで最も一般的な非同期動作の1つであるAjaxリクエストの開始を例にしてみましょう。 Ajaxリクエストを開始するためのブラウザAPIは、非同期APIを提供しています:

jQuery.getJSON('/posts/1', function(post) { 
  $("#post").html("<h1>" + post.title + "</h1>" + 
    "<div>" + post.body + "</div>"); 
}); 

純粋なjQueryアプリケーションでは、このコールバックを使用してDOMを自由に変更できます。

イベントベースのMVCフレームワークを使用する場合、ロジックはコールバックから取り出され、モデルとビューオブジェクトに入れられます。 これによってだいぶ改善されますが、それでも非同期コールバックを明示的に処理する必要性はなくなりません:

Post = Model.extend({ 
  author: function() { 
    return [this.salutation, this.name].join(' ') 
  }, 
  toJSON: function() { 
    var json = Model.prototype.toJSON.call(this); 
    json.author = this.author(); 
    return json; 
  } 
}); 
PostView = View.extend({ 
  init: function(model) { 
    model.bind('change', this.render, this); 
  }, 
  template: _.template("<h1><%= title %></h1><h2><%= author %></h2><div><%= body %></div>"), 
  render: function() { 
    jQuery(this.element).html(this.template(this.model.toJSON()); 
    return this; 
  } 
}); 
var post = Post.create(); 
var postView = PostView.create({ model: post }); 
jQuery('#posts').append(postView.render().el); 
jQuery.getJSON('/posts/1', function(json) { 
  // set all of the JSON properties on the model 
  post.set(json); 
}); 

この例では特別なJavaScriptライブラリを使用していませんが、イベント駆動型MVCフレームワークの典型的な方法で実装されています。非同期イベントの構成は実装されていますが、非同期の振る舞いはコアの手続きモデルのままです。

エンバーのアプローチ

全体として、Emberは明示的な非同期動作を排除することを目指しています。このように、Emberは複数のイベントを同じ結果でマージすることができます。

また、ほとんどのタスクを実行するために、手動でイベントリスナーを登録/解除する必要性を排除する高レベルの抽象化を提供します。

この例では通常ember-dataを使用しますが、EmberでAjaxを行うためにjQueryを使用して上記の例をモデル化する方法を見てみましょう。

App.Post = Ember.Object.extend({ 
}); 
App.PostController = Ember.ObjectController.extend({ 
  author: function() { 
    return [this.get('salutation'), this.get('name')].join(' '); 
  }.property('salutation', 'name') 
}); 
App.PostView = Ember.View.extend({ 
  // the controller is the initial context for the template 
  controller: null, 
  template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>") 
}); 
var post = App.Post.create(); 
var postController = App.PostController.create({ content: post }); 
App.PostView.create({ controller: postController }).appendTo('body'); 
jQuery.getJSON("/posts/1", function(json) { 
  post.setProperties(json); 
}); 

上記の例とは対照的に、Emberの実装では、postのプロパティが変更されたときに明示的にオブザーバを登録する必要がありません。

title}}、{{author}}、{{body}} テンプレート要素は、PostController 上の要素に修飾されます。PostController の内容が変更されると、その変更が自動的に DOM に伝わります。

作者に計算プロパティを使用すると、基礎となるプロパティが変更されたときにコールバックで計算を明示的に呼び出す必要がなくなります。

これに加えて、Emberのバインディングシステムは、PostControllerの計算されたプロパティへのgetJSONコールバックで設定された敬称と名前を自動的に追跡し、常にDOMに変更を伝播します。

マイレージ

Emberは多くの場合、変更の伝搬を担当するため、各ユーザーイベントに応じて変更が一度だけ伝搬されるようにします。

App.PostController = Ember.ObjectController.extend({ 
  author: function() { 
    return [this.get('salutation'), this.get('name')].join(' '); 
  }.property('salutation', 'name') 
}); 

敬称と名前に依存することが指定されているので、これらの依存関係のいずれかが変更されるとプロパティは無効になり、DOM内の{{author}}が更新されます。

ユーザー・イベントに反応してこれを実行すると想像してください:

post.set('salutation', "Mrs."); 
post.set('name', "Katz"); 

このような変更によって、計算されたプロパティが2回無効になり、DOMが2回更新されると考えるでしょう。そして実際、イベントドリブンアプローチを使用するフレームワークでは、このようなことが起こります。

Emberでは、計算されたプロパティは一度だけ再計算され、DOMは一度だけ更新されます。

それはどのようにして実現するのですか?

Emberでプロパティに変更を加えた場合、すぐに変更が反映されるわけではありません。それ以外の場合、依存関係があるプロパティは即座に無効になりますが、実際の変更は後で行われるようにキューに入れられます。

salutation属性とname属性の両方に変更を加えると、author属性は2回無効になりますが、キューはこれらの変更をインテリジェントにマージします。

現在のユーザーイベントに対するすべてのイベントハンドラが終了すると、Ember はキューをリフレッシュし、変更を下方に伝搬します。この場合、無効な author 属性は DOM 内の {{author}} を無効にするため、1 回のリクエストで情報を再計算し、自分自身を 1 回だけ更新することができます。

この仕組みがEmberの根底にあります。 Emberでは、あなたが行った変更の副作用が後で発生することを常に想定する必要があります。この仮定をすることで、Emberは1回の呼び出しで同じ副作用をマージできるようになります。

要するに、イベントドリブンシステムのゴールは、リスナーによって生成されるネガティブなユーティリティからデータ操作を切り離すことなので、よりイベントにフォーカスしたシステムであっても、同期の副作用を想定すべきではありません。Emberでは副作用が即座に伝搬しないという事実が、ズルをして誤って別個に保たれるべきコードを結合する動機を取り除きます。

副作用調節

同期の副作用に頼ることはできないので、特定の動作がちょうどいいタイミングで起こるようにするにはどうすればいいか、悩むことになるでしょう。

例えば、ボタンを含むビューがあり、jQuery UI でボタンをスタイリングしたいとします。ビューの append メソッドは、Ember の他のすべてのものと同様に、その副作用を遅らせます。

その答えはライフサイクルコールバックです。

App.Button = Ember.View.extend({ 
  tagName: 'button', 
  template: Ember.Handlebars.compile("{{view.title}}"), 
  didInsertElement: function() { 
    this.$().button(); 
  } 
}); 
var button = App.Button.create({ 
  title: "Hi jQuery UI!" 
}).appendTo('#something'); 

この場合、ボタンが実際にDOMに入ると、EmberはdidInsertElementコールバックをトリガーし、好きなことができます。

ライフサイクル・コールバック・メソッドには、遅延挿入を心配する必要がない場合でも、多くの利点があります。

まず、依存関係の同期が挿入されるということは、appendToの呼び出し元が来て、要素をアタッチした直後に実行する必要がある動作をトリガーしなければならないことを意味します。アプリの規模が大きくなると、同じビューを多くの場所で作成することになり、それぞれの場所との関連性に注意する必要があります。

ライフサイクルコールバックは、ビューをインスタンス化するコードと、コミット・インサートの動作を行うコードの2つの部分の結合を排除します。一般的に、同期の副作用に依存しないことが、全体的に優れた設計につながることがわかりました。

第二に、ビューのライフサイクルに関するすべてがビュー自体の内部にあるため、EmberではオンデマンドでDOMの一部を再レンダリングすることが非常に簡単です。

例えば、このボタンが{{#if}}ブロック内にあり、Emberがメインブランチからelseセクションに切り替える必要がある場合、Emberは簡単にビューをインスタンス化してライフサイクルコールバックを呼び出すことができます。

Emberは完全に定義されたビューを定義させるため、ビューの作成と挿入を適切に制御します。

また、DOMに関連するコードはアプリケーションの一部にしかラップされないため、コールバック以外のレンダリング処理の部分でEmberの自由度が増します。

オブザーバー

まれに、プロパティの変更が伝わった後に特定のアクションを実行したい場合があります。前節で説明したように、Emberにはプロパティ変更通知にフックする仕組みが用意されています。

コール」の例に戻りましょう。

App.PostController = Ember.ObjectController.extend({ 
  author: function() { 
    return [this.get('salutation'), this.get('name')].join(' '); 
  }.property('salutation', 'name') 
}); 

作者が変更されたときに通知を受け取りたい場合は、オブザーバーを登録します。をビュー関数として表します:

App.PostView = Ember.View.extend({ 
  controller: null, 
  template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"), 
  authorDidChange: function() { 
    alert("New author name: " + this.getPath('controller.author')); 
  }.observes('controller.author') 
}); 

Emberがオブザーバーをトリガーするのは、変更が正常に伝搬された後です。この場合、たとえ敬称と名前の両方が変更されたとしても、EmberはユーザーイベントごとにauthorDidChangeを1回だけ呼び出すことになります。

これにより、すべてのプロパティ変更を強制的に同期させることなく、プロパティ変更後にコードを実行できる利便性が得られます。これは基本的に、計算されたプロパティの変更に対して手作業が必要な場合、Emberのバインディングシステムと同じようにマージする利便性が得られることを意味します。

最後に、オブジェクト定義の外でオブザーバーを手動で登録することもできます:

App.PostView = Ember.View.extend({ 
  controller: null, 
  template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"), 
  didInsertElement: function() { 
    this.addObserver('controller.name', function() { 
      alert("New author name: " + this.getPath('controller.author')); 
    }); 
  } 
}); 

とはいえ、オブジェクト定義構文を使用すると、オブジェクトが破棄されたときにEmberは自動的に観測期間を破棄します。例えば、{{#if}}文がtrueからfalseに変わった場合、Emberはブロック内で定義されたすべての試みを破棄します。この処理の一環として、Emberはすべてのバインディングとインラインオブザーバも切断します。

手動でオブザーバを定義した場合は、必ず削除する必要があります。一般的には、オブザーバを作成したコールバックとは逆のコールバックでオブザーバを削除します。この場合、willDestroyElementコールバックを削除します。

App.PostView = Ember.View.extend({ 
  controller: null, 
  template: Ember.Handlebars.compile("<h1>{{title}}</h1><h2>{{author}}</h2><div>{{body}}</div>"), 
  didInsertElement: function() { 
    this.addObserver('controller.name', function() { 
      alert("New author name: " + this.getPath('controller.author')); 
    }); 
  }, 
  willDestroyElement: function() { 
    this.removeObserver('controller.name'); 
  } 
}); 

initメソッドにオブザーバーを追加した場合は、willDestroyコールバックでオブザーバーを破棄します。

全体として、この方法で手動オブザーバーを登録することはほとんどありません。メモリ管理を安全にするために、オブザーバーは可能な限りオブジェクト定義の一部として定義することを強く推奨します。

Read next

蘇寧雲翔O2O転換レイアウト賈と企業文字クラウドはスピードアップする

最近、JiaheとSuning Yunchangは音声クラウドプロジェクトの協力を開始しました。佳和の "企業クラウド "統一通信システムは蘇寧にローカル音声サービスを提供し、全店舗と支店間の音声通信機能を実現し、2年以内に3万台の電話へのアクセスを実現する予定。

Jul 26, 2016 · 2 min read