今まで、fetchとかthenとか、他のコードみて雰囲気で使ってたけど、つまるところ、何がどうなってるのか、ざっくりわかっておきたくなったので、今更ながらちょっと勉強した、その備忘メモ。
JavaScriptはシングルスレッドだけど非同期実行の仕組みがある。
古くは setTimeout
だけど、今はPromise
とThenable
なる概念があって、 fetch()
とかがこれで実装されている。
MDNでは、ある非同期処理Promise
は「待機 (pending) 」「履行 (fulfilled)」「拒否(rejected)」のいずれかの状態を持つと説明される。
意味がとりにくいので、ここではそれぞれを「未完了」「完了:成功」「完了:失敗」とよびかえることにする。
「完了:成功」「完了:失敗」の状態においては、付随して値を持てる。成功時は処理結果を表す実際の値(計算結果など)、失敗時は失敗理由(例外オブジェクトなど)を持たせるのが一般的な使い方と思われる。
C++だと、Promise
は std::promise<T>
、Thenable
が std::future<T>
、
C# だと、 Promise
が Task<T>
、Thenable
が IAsyncResult<T>
に対応する感じ。
=====
非同期処理を開始する関数は、その結果を受け取る thenable
を返す。
非同期処理を待つ側は、戻り値の Thenable
の .then()
method に callback を登録しておくと、完了したときに呼んでくれるようになる。
※ then()
を呼んだ時点では、処理が予約されるだけであって、そこで制御が待つわけではない。これは、 setTimeout
の呼び出しに似ている。そこで待つかのように書きたいときは await
を使う。(ただし、await
する関数はasync
である必要がある。後述)
const thenable = new Promise(
(resolve, reject) => {
setTimeout(() => {
if (confirm("Success?"))
resolve('Success!'); // promise を 成功状態にする。値は 'Success!'
else
reject(new Error("Failed!")); // promise を失敗状態にする。値は Error
}, 1000);
});
thenable
.then(
(value) => { console.log("SUCCESS: ", value); return value; }, // value を伝播
(reason) => { console.log(" FAILED: ", reason); throw reason; } // reason を伝播
)
.catch(
(reason) => { console.log(" CATCH: ", reason); throw reason; } // reason を伝播
)
.then(
(value) => { console.log("SUCCESS: ", value); return value; }, // value を伝播
(reason) => { console.log(" FAILED: ", reason); return 42; } // error を握りつぶす
)
.then(
(value) => { console.log("SUCCESS: ", value); return value; }, //
(reason) => { console.log(" FAILED: ", reason); throw reason; } // 呼ばれない
)
.finally(
() => { console.log(" FINALLY. "); }
)
Thenable
なインターフェースを持つオブジェクトは then()
method を持つ。
then()
method の基本的な使い方は、「引数を1つとるコールバック関数」を最大2つとる。
その Thenable
が指す非同期処理が、
- 「成功」状態になったら1番目のコールバックを呼び出す
- 「失敗」状態になったら2番目のコールバックを呼び出す
then()
method は、then()
メソッドに渡された関数が……
Thenable
を返した場合、それを返す。Thenable
でないものを返した場合、暗黙的に、それを「完了:成功」状態のPromise
で包んでThenable
として返す。- 例外を送出した場合、暗黙的に、それを「完了:失敗」状態の
Promise
で包んでThenable
として返す。
この挙動によって、 then(...).then(...).then(...)
とメソッドチェーンをつなぐことができる。
非同期実行の必要のないコールバックを途中に挟みたいとき、手づからPromise
で包む必要がないのも便利。
成功した場合は値を素通りさせ、then
の第2引数だけを渡したい場合に使う。すなわち
.then((t) => t, (reason)=>...)
を簡単かつわかりやすく書いたものと思えばいいかしら。
- エラーを適切に処理し、あるいは握りつぶしたければ、何らか値を返せば後続する
then
は、その値で「成功」 fullfilled 続行。 - ログ出しなどしてそのままエラー状態を維持したいときとかは、再
throw
すれば、後続のthen
はその値で「失敗」 rejected 続行。
成功してても失敗しててもどっちの場合でも何かしたいとき使う。
finally
に渡したコールバックハンドラが何かをthrow
すると、後続はその値で rejected。それ以外は context な promise
そのまま、(成功/失敗の状態変更なしに)続行。
Promise
コンストラクタに非同期実行したいコールバックを渡す。
このコールバックの引数として、2つの関数 (resolve, reject)
が渡される。
const thenable = new Promise(
(resolve, reject) => {
if (confirm("Success?"))
resolve('Success!'); // promise を 成功状態にする。値は 'Success!'
else
reject(new Error("Failed!")); // promise を失敗状態にする。値は Error
});
コールバックで処理がおわったら
resolve
を呼んで、そのPromise
を「完了:成功」状態にするか、reject
を呼ぶか何かをthrow
して、「完了:失敗」状態にする ように実装すると、then
が発火する。
Prmise.all( [p0, p1, p2, ... ] )
: 全部成功- 全部が「成功」したら成功。成功時の値は配列で得られる。
[ p0の結果, p1の結果, p2の結果, ...]
- いずれかが「失敗」したら失敗。失敗時の値は最初に報告された失敗の値
- 全部が「成功」したら成功。成功時の値は配列で得られる。
Promise.any( [p0, p1, p2, ... ] )
: どれか成功- いずれかが「成功」したら成功。成功時の値は最初に報告された成功の値
- 全部が「失敗」したら失敗。失敗時の値は
AggregateError
にまとめられる。
Promise.race( [p0, p1, p2, ... ] )
: 最初の完了- いずれかが「成功」したら成功。成功時の値は最初に報告された成功の値
- いずれかが「失敗」したら失敗。失敗時の値は最初に報告された失敗の値
Promise.allSettled( [p0, p1, p2, ... ] )
: 全部完了- 常に成功。成功時の値はそれぞれの完了値。
status
, によってvalue
かreason
かどっちかを持ったオブジェクト。こんな。
[ { status: "fulfilled", value: 3 } , { status: "rejected", reason: "foo" } ]
- 失敗しない。
- 常に成功。成功時の値はそれぞれの完了値。
関数宣言のときに async
キーワードを付けると、その関数本体を Promise
で囲んでくれる。その関数の呼び出しを、thenable
を返すので、.then()
とかで待てるようになる。
await
式は、async function
の中でだけ使える。
await `Promise-Expression`
thenable
を待ちながら、呼び出し元に制御を返す。
await
は thenable
でないものに await
をつけても合法。その場合は直ちに履行される。
こっちは、function*
の async
版。ジェネレータとそれを受けるfor
ループを非同期にできる。っていうか、JavaScriptにジェネレータ構文あったの知らなかった……