BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ ニュース Promises: ブラウザーJavaScriptの新しい非同期標準になるか?

Promises: ブラウザーJavaScriptの新しい非同期標準になるか?

原文(投稿日:2014/05/02)へのリンク

JavaScriptを使う誰もが基本的な作業よりさらに進むと遭遇するのが非同期プログラミング: 関数がすぐに結果を返すのではなく、後で結果が準備できたら呼び出されるコールバック関数を渡す。非同期APIを使用してどのように巨大なアプリケーションを構築するかはJavaScriptの世界で今も進行中の議論である。しかし最近、(最新バージョンのChrome、Firefox、Operaではすでに利用可能な)EcmaScript 6にネイティブのpromisesが追加され、将来的にブラウザーAPIにそれらが採用されていく。これで議論に終止符を打つことができるだろうか?promisesがブラウザーJavaScriptの"ネイティブ"になった今、非同期アプリケーションコードの新しい標準になるのだろうか?

問題

非同期プログラミングとはなにか?Webページでサーバーからmydata.json というファイルを読み込みたいとする。ブラウザーのWeb開発になれていない場合、このようなAPIを記述してしまうだろう:

var result = http.get("/mydata.json");
console.log("Got result", result);

しかしWebにおいては、これは不可能か熟考すべき悪いプラクティスである。なぜならブラウザーは、主にシングルスレッドのランタイム環境を提供している: Webページの描画、イベントの処理とロジックの実行の両方のためにひとつのスレッドしか持っていない。結果、そのスレッドで高負荷または遅いなにかをすると、ブラウザーはその間フリーズしてしまうことになる。つまり、もしJSONファイルの読み込みに2秒かかる場合、ブラウザーはHTTP呼び出しを待つ以外のことができなくなってしまう。これはよい状態とは言えない。この問題を回避するために、JavaScriptの高負荷な処理は 非同期 APIとして公開されている。

一般的には、メインスレッドを応答を待つためにブロックするのではなく、読み込みが終わった後で呼び出される関数を渡すアイディアがある。結果、HTTP呼び出しが続いている間、ブラウザーは他のことを継続することができる。クラシックなXmlHTTPRequestの例を見ていこう:

var req = new XMLHttpRequest();
req.open("GET", "/mydata.json", true);
req.onload = function(e) {
    var result = req.responseText;
    console.log("Got result", result);
};
req.send();

このパターンは、リクエストオブジェクトを作るために、loadイベントをリッスンするイベントリスナーをアタッチしている。これが起動すると、作業を継続することができる。

request-object-with-event-emittersパターンは、ブラウザーAPIでは一般的だが、ブルアザーAPIで使われている他のパターンも存在する。たとえば、ユーザーの現在位置を取得するジオロケーションAPIを見てみよう:

navigator.geolocation.getCurrentPosition(function(result) {
    console.log("Location", result);
}, function(err) {
    console.error("Got error", err);
});

getCurrentPositionはリクエストオブジェクトを返すでのではなく、複数のコールバック関数を受け取り、ひとつめはユーザーの位置の取得成功時じ呼ばれ、ふたつ目は失敗したときに呼ばれる。

サーバーサイドのnode.jsは、非同期APIへの最後の引数として、ふたつの引数(エラーと結果)を受け取るひとつのコールバック関数を引き渡すデファクトスタンダードである。たとえば、以下はnode.jsを使ってファイルを読み込み方法である:

var fs = require("fs");

fs.readFile("mydata.json", function(err, content) {
    if (err) {
        console.error("Got an error", err);
    } else {
        console.log("Got result", content);
    }
});

これらすべてのパターンには課題がある。たとえば:

  • エラーの伝搬は手動である。同期プログラミングスタイルでは、エラーの処理と伝搬にthrow, try, catchを使用することができる。これらの言語レベルのメカニズムは非同期フローでは動作しない。さらに悪いことにエラー処理を忘れてしまうとエラーの結果は簡単に消えてしまうか、処理がクラッシュしてしまう。
  • コールバック関数は呼ばれないか複数回呼ばれる。非同期関数を書く場合、高い頻度でコールバック関数のコールバックを忘れるか、複数回呼び出してしまう。どちらも問題のデバッグを非常に困難にする。
  • コールバック関数は自然に深くネストされたコールバックになる。これは回避することができるが、非同期コードでは一般的な問題である。

Promise

Promiseは非同期コードを単純化することを目指している。promiseを使ったAPIはコールバック引数を取るのではなく、代わりにpromiseオブジェクトを受け取る。promiseオブジェクトは少量のメソッドのみを持ち、もっとも重要なのはthen メソッド(promiseは時々"thenables"と呼ばれる;)である。 thenメソッドはひとつかふたつの引数を取り、ひとつめはpromiseがresolved(成功)したときに呼ばれ、ふたつ目はpromiseがrejected(結果がエラー)の時に呼ばれる。これらのコールバック関数のいずれかは以下のようにできる:

  • 新しいpromiseを返す。この場合、promise (解決または拒否)の結果は、新しく返されたpromiseに委任される。実質的に深いネストなしで簡単に非同期呼び出しのチェーンを使うことができる。
  • (promiseじゃない)値を返す。この場合promiseはこの値に解決される。
  • エラーをスローする。 関数の中で例外がthrowされた場合、promiseを拒否したことになる。

例を見てみよう。ユーザーのロケーションを取得して、そのロケーションを持ってサーバーへのAJAX呼び出しをしたいとする。以下のコードは通常の非同期プログラミングのスタイルでそれを実行している:

navigator.geolocation.getCurrentPosition(function(location) {
    var req = new XMLHttpRequest();
    req.open("PUT", "/location", true);
    req.onload = function() {
        console.log("Posted location!");
    };
    req.onerror = function(e) {
        console.error("Putting failed", e);
    };
    req.send(JSON.stringify(location.coords));
}, function(err) {
    console.error("Got error", err);
});

このようにここではふたつのエラーハンドラーを持っている。それではこれらのAPIのpromiseベースのバージョンを仮定したものを見てみよう:

navigator.geolocation.getCurrentPosition().then(function(location) {
    var req = new XMLHttpRequest();
    req.open("PUT", "/location", true);
    return req.send(JSON.stringify(location.coords));
}).then(function() {
    console.log("Posted location!");
}).then(null, function(err) {
    console.error("Got error", err);
});

promiseベースのバージョンで注意するべきこと:

  • ネストの深さは1のみである。
  • エラー処理は1箇所に集中している。最初の呼び出しでエラーが発生したら、thenがそれを処理するまで伝搬していく。

Promiseにはさらに多くの利点があるので、この記事の最後にあるその他マテリアルへのポインターを見て欲しい。

将来

Chrome 32, Firefox 29, Opera 19でPromiseコンストラクターを使ってブラウザーにPromiseが組み込まれている。さらに将来のブラウザーAPIはそれらを使用する。例えば:

promiseはブラウザーAPIでさらに普及し、さらに多くのブラウザーベースアプリケーションやライブラリーはそれを採用するだろうか?jQueryはすでにpromiseに似たものをdeferredとしてサポートしている。さらにEcmaScript 6のジェネレーターでpromiseはさらに多くの利益を得る。それは: 同期に見えるコードで非同期に実行されるコードを書く能力だ。

JavaScriptの新しいネイティブPromiseについてとその利点についてさらに学ぶにはJake Archibald氏のHTML5Rocksの記事が起点としてはよいだろう。 もし古いブラウザーを対象にしているのであれば、小さなpolyfillが提供されているDomenic Denicola氏はpromiseに関する多くのすばらしいトークを提供し、長い間promiseの支持している。promiseのAPI仕様は、Mozillaの開発者ネットワークをベースにしたthe Promises/A+ proposalに記載されている。

この記事に星をつける

おすすめ度
スタイル

BT