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/await
はTask
により実現されていますが、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
として表現された手続きがすでに成功もしくは失敗していた場合での、コールバック関数は呼び出されます。
Promise
はQ、RSVP.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の合成
加えて、Promise
のthen()
呼び出しはチェインさせることで、データ変換の実行や非同期処理のシーケンシャルな実行を実現できます:
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)
とした場合func1
とfunc2
のいずれか片方しか実行されないのに対して、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"の中では他にもいろいろなことが述べられていますが、ひとまずこんな感じでしょうか。