Fork me on GitHub

あなたが読むべきJavaScript Promises

Edit on GitHub 編集履歴を見る

はじめに

この記事は、 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

Promisesについてよくまとまっていて、これを読めばひと通りの流れがわかるようになってる記事

アウトライン

  • 何故Promisesなのか?
    • 画像がロードの成否でコールバックを呼びたい時
    • "Event"を使う場合はload,errorイベントで設定できる
      • 既にロード済みの場合は発火しない
      • そのためcomplete プロパティで判定して分岐する必要がある
      • "Event" は いつもベストというわけではない
    • promiseは一度だけ呼ばれる 成功失敗 のコールバックを設定出来る
    • 既にロード済みでも、成功失敗 のコールバックは呼ぶことが出来る
  • Promiseの用語
    • promiseが持つ状態の種類
    • (*Kerrick Longによるスライド がわかりやすい)
  • JavaScriptでのPromises
    • WinJS.Promise, RSVP.js, Q, when.jsのようなライブラリがある
    • 上記のライブラリは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
  • 他のライブラリとの互換性
    • 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はコールバックに渡すものが多少異なる
  • 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){ }) と書くことができる
      • catchthen(undefined, func) のシンタックスシュガー
    • JavaScriptの例外とPromises
      • Promiseコンストラクタ内で例外が投げられた場合
      • 暗黙的にrejectがエラーオブジェクト共に呼ばれる
      • つまりPromiseコンストラクタ内で例外がおきても then/catch捉えることができる
    • Error handling in practice
      • promiseではない場合はtry-catchを使った方法を使う
      • promiseの場合は .catch() のchainを書くだけとシンプル
  • 並行と逐次実行
    • Promisesで複数の非同期処理を扱う方法について
    • promise.then を使った非同期処理
    • ループの中で promise.then の非同期処理しても上手く回らないケース
      • ループでも動くように逐次実行用のpromiseオブジェクトを作る方法
      • array.reduceを使ってmutableな変数を使わないように逐次実行
    • Promise.all を使ってpromiseオブジェクトの配列をまとめて処理する
    • PromisesとGeneratorsを合わせた使い方
  • Promise API Reference

▶ JavaScript Promises: Thinking Sync in an Async World - YouTube

Promisesにおけるデータの流れについてよくまとまっているスライド。

promiseの各メソッドについても紹介されているので全体像をさっとみるのに適している。

アウトライン

  • Promise Basic
  • 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 の中での thisbind
    • Absorbed Rejections
      • .catchconsole.error を bindする

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 は例外的な場合に使うべきである
    • Asynchronous Algorithms
      • (この辺は議論中なのでちゃんと原文を見ましょう)
      • 正常なコントールフローを管理する
      • 不必要なタスクのキューとして使わない
      • 成功やエラーの種類のために、追加で新たに別のコールバックは作らない
        • onFulfilled, onRejectedに渡す値で分けるべき
    • Promise Arguments
      • promiseオブジェクト を引数に受ける関数は Promise.cast すべきである。
  • Shorthand Phrases
    • 仕様を読み書きするときに便利なフレーズ集

応用/実践

訳: Promiseアンチパターン - くじら公園

Promisesのアンチパターンについて書かれている記事。

アンチパターンとそれを改善したパターンについて書かれているので、実際に書いてみて疑問に思ったりするパターンについてまとまっている。

アウトライン

  • ネストしたPromises
  • thenの返り値はそれぞれ新しいpromiseを返すために起きるエラーハンドリングのミス
  • 配列を渡してそれぞれについてpromiseを使った処理をする場合に起きる問題
  • 過剰なエラーハンドリング
    • then()の第二引数(onRejected)でもエラーハンドリングはできるが
    • 第一引数の onFulfilled 内でエラーが起きた場合に問題が起きる
  • promiseを返す関数であることを忘れて、さらにPromiseで包んでしまう問題

Promisesとコールバックを使った実装の比較やPromisesのパターンについて書かれているチュートリアルサイト。

(途中で出てくるfs.readFileAsync(file)BluebirdPromise.promisifyAllを使ったpromiseを返すバージョンの事)

Promise nuggetsの著者によって書かれたPromiseの利点についての記事。

throw-safeなエラーハンドリング、パフォーマンスとメモリ消費、promise.nodeifyを使ったコールバックスタイルとの互換性、Promiseの書き方やユースケース等について書かれている。

ライブラリ/polyfill/ツール

PromisesはChrome/Opera/Firefox等、一部ブラウザでしか実装されていませんが、 ECMAScript Language Specificationに基づく実装や、Promises/A+の実装ライブラリ等があります。 (どちらも基本的に挙動はほぼ同じで、API名が異なる部分がある程度です)

また、Advancedとなっているものは、基本的な実装は同じですがよくあるパターン等を拡張したメソッド等が実装されています。

またどちらのライブラリもAPIドキュメントが充実しているので、このライブラリを直接使わない場合でも見ておくといいかもしれません。

ES6 polyfill

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について深く理解しやすいかもしれません。

この記事へ修正リクエストをする
JSer.info Slackに参加する