今まで、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にジェネレータ構文あったの知らなかった……