M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

TS v2.1のasync/awaitのまえにPromises/A+について学ぶ

先日、TypeScript v2.1がリリースされ、そのリリースノートのなかの"Downlevel Async Functions"セクションで、ECMAScript v3(ES3)やv5(ES5)のランタイムでもasync/awaitが使用可能になった(ただしそれらの環境にPromiseを導入するポリフィルのJSコードが読み込まれていることが前提)という記載がありました。

以前からasync/awaitの機能は提供されていましたが、それはコンパイルのターゲット・ランタイムがECMAScript v6(ES6/ES2015)であることが条件でした。TS v2.1ではこの条件が緩和されたわけです。これは面白いことになってきました。

C#においてはasync/awaitTaskにより実現されていますが、TypeScriptにおいてはPromiseにより実現されています。なるほどわかりやすい対応関係です。ではまずPromiseについてやはりある程度きっちり知っておかなくては、ということで、Google Developersの"JavaScript Promises: an Introduction"(by Jake Archibald)という記事を読んでみました。

「お集まりの紳士淑女の皆さん、Web開発の転換点がやってきました」から始まるだいぶふざけた感じの前置きのあと、イベント・メカニズムとの比較を通じてPromiseについての解説が行われています。

イベント・メカニズムの課題

まずはイベント・メカニズムの問題点。次のようなコードではイベントリスナーの設定の前にイベントが起きてしまった場合、リスナー関数は実行されません:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

"load"イベントについて対策を施したものは次のようになりますが、これでも依然として"error"イベントには同じ問題が残ります:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

そしてWebページ上の画像は当然1つだけとは限りません。"load""error"を監視すべきリソースごとにこんなコードを書くのは現実的ではありません。

Promiseによる課題克服

そこでPromiseが登場します。Chrome 32、Opera 19、Firefox 29、Safari 8、そしてMicrosoft Edgeにおいてすでに、Promiseはデフォルトで使用可能になっています。Promiseをサポートしていなかったり、部分的にしかサポートしていない環境向けにはポリフィルが提供されています。

もしIMG要素がPromiseインスタンスを返すready()メソッドを持っているとすれば(これは仮定の話ですが):

img1.ready().then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()]).then(function() {
  // all loaded
}, function() {
  // one or more failed
});

Promiseはイベントのメカニズムとはいくつかの点で異なります:

  • Promiseは何度も成功したり、何度も失敗したり、成功のあとに失敗したりといったことは起こりません。Promiseとして表現された手続きに対して成功時と失敗時のそれぞれのコールバック関数を設定できますが、それらはいずれか片方、それもただ1回しか呼び出されません。
  • そして、もしコールバック関数が設定された時点でPromiseとして表現された手続きがすでに成功もしくは失敗していた場合での、コールバック関数は呼び出されます。

PromiseQRSVP.jsなどいくつかのライブラリで実装されてきmしたが、それらとJavaScriptにネイティブに導入されたPromiseとが共有する標準的な挙動はPromises/A+として仕様化されています。

JavaScriptネイティブのPromiseの作成は次のように行います:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

なお、明示的にreject()を呼び出さなくても、エラーがスローされた場合はreject()が自動的に呼び出されます。作成したPromiseは次のようにして使用できます:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

PromiseとPromiseの合成

加えて、Promisethen()呼び出しはチェインさせることで、データ変換の実行や非同期処理のシーケンシャルな実行を実現できます:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

catch()メソッドはthen(undefined, func)メソッドと同じように機能します。注意が必要なのは、単にthen(func1, func2)とした場合func1func2のいずれか片方しか実行されないのに対して、then(func1).catch(func2)とした場合func1の処理が失敗するとfunc2も呼び出されるということです(ちなみに大元のPromiseが失敗した場合func1は呼び出されず、func2のみが呼び出されます)。

さらに、then()の第2引数を使用するにせよcatch()の第1引数を使用するにせよ、一旦エラー・コールバックがエラーを処理すると、その次にチェインされているthen(func1, func2)ではfunc1が呼び出されます。

then()はシーケンシャルな実行を提供する機能ですが、Promise.all([p0, p1, p2, ...])は個々の並行に実行される処理からなる新しいPromiseを生成します。

"JavaScript Promises: an Introduction"の中では他にもいろいろなことが述べられていますが、ひとまずこんな感じでしょうか。

m12i.hatenablog.com