はじめに
この記事は、 JavaScript/ES6 promisesについてを理解するために読んだ方がよいと思われる記事やスライド等を紹介しています。
PromisesやDeferredといった言葉を非同期処理の話などで聞いた事があるかもしれませんが、 現在Promisesは次のECMAScriptの言語仕様として策定が進められています。
まだES6は策定段階ですが、既にPromisesについてはpolyfillとして利用できるライブラリ等もあり、また他のライブラリ内でもjQuery.Deferred()やAngularの$q等類似する実装が存在します。
そのため、Generators等に比べると今すぐ使えるし、既に使われている機能といえると思います。
ES6 PromisesはAPIとしてはそこまで数はなく、また類似する実装でも基本的にAPI名が異なるだけで挙動は同様なため(jQuery Defferdはちょっと異なりますが…)、ES6 Promisesについて学ぶことは今すぐ使える知識なので、この機会にPromisesについて調べてみるといいかもしれません。
紹介している記事は長いものも多いので、記事の簡単な概要とアウトラインをつけてあります。
特に読むべき順番などはないですが(上から順に読みやすい感じにしてはいます)、気になるワードが出てきたものから読んでみるのもいいですね。
追記
ここで出てくる内容をカバーしたJavaScript Promiseの本という無料の電子書籍を書きました。
こちらも合わせて読むといいかと思います。
紹介記事
JavaScript Promises: There and back again - HTML5 Rocks
http://www.html5rocks.com/en/tutorials/es6/promises/
日本語版: JavaScript Promises: There and back again - HTML5 Rocks
Promisesについてよくまとまっていて、これを読めばひと通りの流れがわかるようになってる記事
アウトライン
- 何故Promisesなのか?
- 画像がロードの成否でコールバックを呼びたい時
- "Event"を使う場合は
load
,error
イベントで設定できる- 既にロード済みの場合は発火しない
- そのため
complete
プロパティで判定して分岐する必要がある - "Event" は いつもベストというわけではない
- promiseは一度だけ呼ばれる 成功、失敗 のコールバックを設定出来る
- 既にロード済みでも、成功、失敗 のコールバックは呼ぶことが出来る
- Promiseの用語
- promiseが持つ状態の種類
- (*Kerrick Longによるスライド がわかりやすい)
- JavaScriptでのPromises
- WinJS.Promise, RSVP.js, Q, when.jsのようなライブラリがある
- 上記のライブラリはPromises/A+と呼ばれる仕様を元にしてる
- APIのインターフェースは微妙に異なるが、挙動はPromises/A+に準拠している
- jQuery.Deferred()のようなDeferredと呼ばれる似たようなものもある。
- jQuery.Deferred は Promises/A+ に準拠したものではなく、挙動が異なる部分もある
- Promiseを使った実装の流れ
Promise
コンストラクタは2つのパラメータを持つ1つのコールバックを引数に取るvar promise = new Promise(function(resolve, reject) { … }
- 全て問題なく実行できた場合は
resolve
を呼び、それ以外はreject
を呼ぶ - エラー時でも
throw
しないで、reject
にErrorオブジェクトを渡して呼ぶ - Errorオブジェクトを使うことでスタックトレース等デバッグツールに優しい感じになる
- promiseのインスタンスは
then
というコールバックを設定するメソッドを持っている - 結果を使う場合は
promise.then
で成功、失敗のコールバックを設定してあげる。
- JSのpromiseはDOM "Futures" という名前で始まり、"Promises" にリネームされた
- そのためDOMでも使えるし、現にいくつかのDOM APIの仕様でも使われてる
- (*ECMAScript6の仕様の方に入ってるので、DOMがないところでも使える)
- ブラウザサポートとpolyfill
- (* 最新の状態はCan I use…等を見ましょう)
- 他のライブラリとの互換性
- JS Promises APIは
then
というメソッドを持つオブジェクトを promise-like (or thenable)と呼ぶ - promise-likeなものをPromiseのインスタンスとして扱えるようにする
Promise.cast
が利用できる Promise.cast
を使うことでQのPromiseやjQueryのDeferredも扱えるvar jsPromise = Promise.cast($.ajax('/whatever.json'));
- ただしjQueryのDeferredはコールバックに渡すものが多少異なる
- JS Promises APIは
- Complex async code made easier
- XMLHttpRequestをPromise化する
XMLHttpRequest
をラップしてpromiseオブジェクトを返すようにする例
- Chaining
then
を使うことで非同期処理の後に別の処理を追加できるthen
のコールバックで値を変換することも出来る(return
で変換した値を返すだけ)then
の返り値(*1)に対してもthen
(next-then
)で処理を追加することができる。- next-
then
に設定されたコールバックは*1の値がsettles (succeeds/fails)になった時に呼ばれる
- Error handling
then
はsuccessとfailureのコールバックを指定できるがエラーについてのコールバックだけを書きたい場合catch(function(response){ })
と書くことができるcatch
はthen(undefined, func)
のシンタックスシュガー
- JavaScriptの例外とPromises
- Promiseコンストラクタ内で例外が投げられた場合
- 暗黙的にrejectがエラーオブジェクト共に呼ばれる
- つまりPromiseコンストラクタ内で例外がおきても
then
/catch
で捉えることができる
- Error handling in practice
- promiseではない場合は
try-catch
を使った方法を使う - promiseの場合は
.catch()
のchainを書くだけとシンプル
- promiseではない場合は
- 並行と逐次実行
- Promisesで複数の非同期処理を扱う方法について
promise.then
を使った非同期処理- ループの中で
promise.then
の非同期処理しても上手く回らないケース- ループでも動くように逐次実行用のpromiseオブジェクトを作る方法
array.reduce
を使ってmutableな変数を使わないように逐次実行
Promise.all
を使ってpromiseオブジェクトの配列をまとめて処理する- PromisesとGeneratorsを合わせた使い方
- Promise API Reference
- PromiseのAPIリファレンス
- (* Promise - JavaScript | MDN )
JavaScript Promises - Thinking Sync in an Async World // Speaker Deck
https://speakerdeck.com/kerrick/javascript-promises-thinking-sync-in-an-async-world
▶ JavaScript Promises: Thinking Sync in an Async World - YouTube
Promisesにおけるデータの流れについてよくまとまっているスライド。
promiseの各メソッドについても紹介されているので全体像をさっとみるのに適している。
アウトライン
- Promise Basic
- Promises/A+の実装ライブラリ
- Promise States
Pending
,Fulfilled
,Rejected
の3つPending
-(Value)->Fulfilled
Pending
-(Reason)->Rejected
- Promise Prototype Methods
Promise.prototype.then
Promise.prototype.catch
-promise.then(null, onFailure)
と同義Promise.cast
- オブジェクトをpromiseにするPromise.all
- 全てのpromiseがresolve
された時に次へ行くPrmises.race
- ひとつでもpromiseがresolve
された時に次へ行く
- Creating Promises
new Promise(function(resolve, reject){/* work */});
- Shortcut
Promise.resolve
Promise.reject
- Advanced Techiniques
then
の中でのthis
とbind
- Absorbed Rejections
.catch
とconsole.error
を bindする
w3ctag/promises-guide
https://github.com/w3ctag/promises-guide
Promisesはいろいろなライブラリで試されてきた概念で、それを元にPromises/A+というコミュニティベースな仕様が立ち上げられた。(Promises/A)
この仕様に対して多くのライブラリが適合するようになり、そして今Promisesは次のECMAScript仕様にも含まれるようになった。
PromisesはWeb Platformにおける非同期処理の一つのパラダイムであり、Streams APIなど他の仕様でも使われつつある。
このドキュメントはどのようにしてPromisesの仕様が作られたか、またPromisesをどのように使うかについて書かれている。
アウトライン
- いつPromisesを使うか
- "One-and-Done Operations" 一度きりの操作の場合Promisesが適切
- 同期的な関数は値を返すか例外を投げる
- 非同期な関数は
promise
を返すpromise
は 値をfulfilled
する or 理由と共にrejected
する
- 今までのweb platformの仕様でも非同期操作は存在した
- それは
onsuccess
/onerror
のようなイベントかコールバックで表現されていた - 一度きりの “Event” としてのPormises
- ある状態になった時に呼び出す目的で “Event” を使った場合に既に状態を満たしてると “Event” が発火しない
- Promisesの場合は
.then(onReady, onFailure)
で既に状態を満たしている場合でもonReady
を呼ぶことが出来る
- いつPromisesを使うべきでないか
- "One-and-Done Operations" ではない時
- 繰り返し起きるEventの場合
- ストリーミングデータを扱う時
- API Design Guidance
- Errorsについて
- Promiseコンストラクタ内で
throw
すべきでない onRejected
にはError
型の値を渡すべきであるonRejected
は例外的な場合に使うべきである
- Promiseコンストラクタ内で
- Asynchronous Algorithms
- (この辺は議論中なのでちゃんと原文を見ましょう)
- 正常なコントールフローを管理する
- 不必要なタスクのキューとして使わない
- 成功やエラーの種類のために、追加で新たに別のコールバックは作らない
onFulfilled
,onRejected
に渡す値で分けるべき
- Promise Arguments
promiseオブジェクト
を引数に受ける関数はPromise.cast
すべきである。
- Errorsについて
- Shorthand Phrases
- 仕様を読み書きするときに便利なフレーズ集
応用/実践
Promise Anti-patterns
http://taoofcode.net/promise-anti-patterns/
Promisesのアンチパターンについて書かれている記事。
アンチパターンとそれを改善したパターンについて書かれているので、実際に書いてみて疑問に思ったりするパターンについてまとまっている。
アウトライン
- ネストしたPromises
then
の返り値はそれぞれ新しいpromiseを返すために起きるエラーハンドリングのミス- http://www.es6fiddle.net/hs08k5sh/ のようなコードはエラーを
catch
できない then
の返り値を返すようにするとcatch
できる http://www.es6fiddle.net/hs08nnaq/
- http://www.es6fiddle.net/hs08k5sh/ のようなコードはエラーを
- 配列を渡してそれぞれについてpromiseを使った処理をする場合に起きる問題
- Parallelism and sequencing - Getting the best of both
Promise.all
はarray.reduce
を使って処理できるようにする
- 過剰なエラーハンドリング
then()
の第二引数(onRejected
)でもエラーハンドリングはできるが- 第一引数の
onFulfilled
内でエラーが起きた場合に問題が起きる
- promiseを返す関数であることを忘れて、さらにPromiseで包んでしまう問題
Promise nuggets
http://promise-nuggets.github.io/
Promisesとコールバックを使った実装の比較やPromisesのパターンについて書かれているチュートリアルサイト。
(途中で出てくるfs.readFileAsync(file)
はBluebirdのPromise.promisifyAllを使ったpromiseを返すバージョンの事)
Why I am switching to promises
http://spion.github.io/posts/why-i-am-switching-to-promises.html
Promise nuggetsの著者によって書かれたPromiseの利点についての記事。
throw-safeなエラーハンドリング、パフォーマンスとメモリ消費、promise.nodeifyを使ったコールバックスタイルとの互換性、Promiseの書き方やユースケース等について書かれている。
ライブラリ/polyfill/ツール
PromisesはChrome/Opera/Firefox等、一部ブラウザでしか実装されていませんが、 ECMAScript Language Specificationに基づく実装や、Promises/A+の実装ライブラリ等があります。 (どちらも基本的に挙動はほぼ同じで、API名が異なる部分がある程度です)
また、Advancedとなっているものは、基本的な実装は同じですがよくあるパターン等を拡張したメソッド等が実装されています。
またどちらのライブラリもAPIドキュメントが充実しているので、このライブラリを直接使わない場合でも見ておくといいかもしれません。
ES6 polyfill
- yahoo/ypromise
- ES6-Promises
- RSVP.jsを元にES6の仕様に合うようにマップしたもの
Promise/A+
Advanced
ツール
- ES6 Fiddle
- Traceurを使ってコンパイルされるため、Promisesをサポートしてないブラウザでもコードを試せる
おわりに
Promisesはコールバックの深いネストを減らせるという見た目的な問題だけではなく、 非同期処理の複雑なエラーハンドリングを分かりやすく書くことが出来る点等もメリットだと思います。
非同期のタイミングで値が入った時にどうするか?という事を書いた器(promiseオブジェクト)をやりとりすることで、 コールバックをネストした時に比べてあちこちに点々とする値ではなく、promiseオブジェクトという器に対して処理を集中して書けることが特徴だと思います。
そのため、器であるpromiseオブジェクトに対して何度もデータを入れるような”Event”や”Stream”と言った動作にはPromisesは不向きであるかもしれません。 必ずしも全てがPromisesで賄えるわけではないため、ひとつの手段として知っておくことが大切と言えると思います。
最後に、この記事は あなたが読むべきPromises by azu · Pull Request #17 · azu/jser.info で議論(という名のほぼ独り言)を元に書かれました。
このissueにはこの記事で紹介してない実装しながら学ぶ系の記事やモナドネタなども候補に出してましたが、今回は入れてないので興味がある人はそちらも見るといいかもしれません。
また、他にも読まれるべきだと思うものがある場合は、あなたが読むべきPromises by azu · Pull Request #17 · azu/jser.infoにコメント等しておけば更新されるかもしれません。
この記事/紹介してる記事がPromisesの理解の助けになればと思います。
追記
これらの内容にテストの話の内容を加えたような、 JavaScript Promiseの本という無料の電子書籍を書きました。
ここで紹介したものと合わせて読むとPromiseについて深く理解しやすいかもしれません。