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 について、正しく解説しているページのほうが少ない。