JavaScript での非同期処理は、ジョブキューにコールバック関数を登録し、それを適宜呼び出してもらう。シングルスレッドでの動作が基本になる。
複数スレッドなどの物理的に同時に走る並列 parallel 処理ではない。並行 concurrent 処理だ。
巷では Promise
の書き方を簡単にしたのが async
/ await
のように解説されていることも多いが、多くの場合誤りか、そうでなくても誤解しやすい。
単純なコールバック関数の例. setTimeout()
はすぐ戻る. 時間が経過したらコールバック関数が呼び出される.
実行結果;
1. setTimeout() returned. 2. Hello
まず setTimeout()
の次の文が実行され、後からコールバック関数が呼び出されている。
イベントハンドラを登録する例。状態が変化したら、コールバック関数が呼び出される。
実行結果;
1. send() returned. 2. onload called.
send()
はすぐ戻る. リソースの fetch に成功したり失敗したときにコールバックされる。
JavaScript の制限として, 非同期処理は try-catch で例外を捕捉できない。そこで、戻り値でエラーかどうか示すのがよく採られる。次は, Node.js でのファイル読み込みの例;
1. readFile() returned. 2. Body: fugaあああ
同様に, readFile()
はすぐ戻る。読み込みが完了したりあるいはエラーになったら、コールバックされる。
非同期処理・エラー状態・正常データをセットで標準化したものです。Promise
は "非同期" 処理で使われるのが前提です。
Promise
オブジェクトを返す関数例えば, fetch()
関数は URL で指し示されたデータを取得します。この関数は、呼び出すと直ちに Promise
オブジェクトを返します。
1. fetch() returned. 2. .then() callback called. Status error: 404 Not Found
Promise
オブジェクトの .then()
でコールバック関数 (handler) を登録します。ここはあくまで登録であって、非同期処理を待ち受けるわけではありません。巷の解説ではしばしば待ち受けるかのように誤解しやすいですが、この区別は重要です。
非同期処理が完了したかエラー状態になったときに, .then()
に与えたコールバック関数 (handler) が実行されます。
正常データとエラー状態のセットを返す方法の標準化としては, Haskell の ハンドラの戻り値が Either
モナドに近いです。Promise
で wrap ラップされる、とかいう解説が多いですが、これも正しくありません。Promise
が wrap するのは, "pending" か実行済みかという状態であって、ハンドラの戻り値ではありません。
Promise
オブジェクトへのハンドラの登録は、次の3種類があります。
.then( onFulfilled )
.then( onFulfilled, onRejected )
.catch( onRejected )
onFulfilled, onRejected はコールバック関数です。それぞれ正常状態、エラー状態になったときに (のみ) 呼び出されます。.then()
, .catch()
とも、直ちに, 新しい Promise
を返します。さらに後続のハンドラを登録できます。
JavaScript では, 非同期処理の例外を同期側から try-catch ブロックで捕捉することはできません。プロミスを使う非同期処理では, 例外が発生すると, 後続の .then()
2引数版か .catch()
に与えたハンドラが呼び出されます。
実行結果
$ node --unhandled-rejections=strict chain2.js 1. Here! .catch(): TypeError: hoge at /home/hori/src_local/js/promise/chain2.js:17:15 .catch(): Error: Something wrong: fuga at Timeout._onTimeout (/home/hori/src_local/js/promise/fake_fetch.js:26:24) at listOnTimeout (internal/timers.js:549:17) at processTimers (internal/timers.js:492:7)
たまたま後ろのエラーのほうが先に完了したため、先に実行されている。
.then()
または .catch()
のハンドラの戻り値は、プロミスである必要はありません。通常の値か例外送出のどちらかが基本です。
プロミスを返した場合は, 特別に, その戻り値のプロミスの "状態" に応じて, 後続のハンドラの呼び出しが決まります。値の wrap を剥がすのではない.
メソッドチェインで, 処理の「順序」を指示することができます。
.then()
(1引数版) は正常時のみ, .catch()
はエラー状態時のみ処理が実行され、それぞれ該当しない時は skip されます。分岐 (decision) は、下表の fork で、片方を .then()
, 他方を .catch()
にすればよい。
| p1 = プロミスを返す関数() p2 = p1.then(...) // 2引数版だと、正常でもエラーでも処理を実行. p3 = p2.then(...) |
| p1 = プロミスを返す関数() p2 = p1.then(...) p3 = p1.then(...) // 複数の .then() を接続してよい |
| p1 = プロミスを返す関数() p2 = p1.then(...) p3 = p1.then(...) // fork p4 = Promise.all([p2, p3]) |
| p1 = プロミスを返す関数() p2 = p1.then(...) p3 = p1.then(...) // fork p4 = Promise.race([p2, p3]) |
次の例は, .then()
を二つ繋げます。fakeFetch()
は fetch()
に似せた関数です。定義は後述しますが、今は本題ではありません。
$ node --unhandled-rejections=strict chain.js 1. Here! 2. p1.then(): Response { body: 'Response of hoge/success', status: 200 } 3. p2.then(): 1
.then()
よりもさらに後ろのスクリプトが先に実行されている点に注意てす。
.catch()
が戻したプロミスに, さらに .then()
や .catch()
を繋げてもよい。.catch()
のハンドラが正常に完了すると後続の .then()
の, 例外を投げると後続の .catch()
のコールバック関数に繋がる.
これは意外な挙動で, そうすると, あるプロミスについて, 先に .catch()
を書くような書き方は上手くいかない。
ハンドラから値を返したとしても, 例外を投げたとしても, いずれも後続のハンドラに繋がるので, ハンドラの中からはメソッドチェインを途中で中断, というのはできない。
最後のハンドラの戻り値を得るには、非同期処理が完了するのを待ち受ける。await
キーワードを使う。
Promise.all()
, Promise.race()
は, 上述のとおり, 待ち受けではありません。
async
, await
は, Promise
の構文糖ではありません。
1. Here! 2. returned: Promise {} 3. p2.then(): Response {type: "basic", url: ...} 4. Results = Response {type: "basic", url: ...}
await
は Promise
オブジェクトを引数に取り, settle されたら、値を取り出すか, 例外を送出します。
await
キーワードは, async function の中でしか書けません。単純な完了待ちではスクリプト全体が止まってしまいます。それを囲む async function をいったん脱出し, 処理が完了する前に Promise を返し、その後ろの処理を続行します。その後、非同期処理が完了したら、ふたたび await
の場所に戻ってきます。
結局、いったん非同期処理を開始したら、非同期ではない側から, 適時に値を取り出すことはできません。
非同期関数は, 新しい Promise
オブジェクトを返すようにします。
Promise
のコンストラクタに非同期処理したい関数を渡します。resolve, reject の2引数を取ります。
プロミスは内部状態を持つ. "pending", "fulfilled" (成功), そして "rejected" (失敗) です。"fulfilled" と "rejected" を合わせて、プロミスが「settle された」といいます。
resolve()
を呼び出すと, プロミスの状態が "fulfilled" になり, .then()
で登録したハンドラが呼び出されます。reject()
を呼び出すと, プロミスの状態が "rejected" になり, .then()
2引数版か .catch()
で登録したハンドラが呼び出されます。
AbortController
を使う。TODO: 書く
See Cancel a JavaScript Promise with AbortController - Bramus! - Medium
最初, 値を wrap または lift したものが Promise
と誤解していたので、モナドかどうかが気になったが、そもそもそれが間違いだった。
これはナンセンスな議論だ; JavaScript の Promise は モナドではない というか, 値を得るには await
じゃない?
ざっと見ると, Promise
について、正しく解説しているページのほうが少ない。