コールバックとPromise・async/await
小さなNode.jsファイルで実行順を表示します。コールバックの呼出し時点を変え、Promiseの確定とthen、awaitでの再開、複数の処理を開始する位置を順に確認します。各ファイルは新しいプロセスで実行します。
説明
非同期処理では、処理の開始と結果を受け取る時点が分かれます。待っている間に呼出し元が進めるため、コードを上から読んだ順と完了した順が同じとは限りません。
小さなNode.jsファイルで実行順を表示します。コールバックの呼出し時点を変え、Promiseの確定とthen、awaitでの再開、複数の処理を開始する位置を順に確認します。各ファイルは新しいプロセスで実行します。
1コールバックを呼ぶ位置
callback.mjsはsync指定ならその場で関数を呼び、それ以外はmicrotaskとして予約します。
cat callback.mjs呼出し時点を変えるコードcallbackは後で使う処理として渡す関数です。ただし、関数を渡しただけで非同期になるわけではありません。呼出し先がその場で呼ぶか、後に予約するかで順序が変わります。
2その場で呼ぶコールバック
syncを指定して実行します。
node callback.mjs同じコードを実行 syncその場でコールバックを呼ぶbefore→value 3→afterです。readValueが戻る前にコールバックが実行されるので、afterはその後になります。
3後へ予約したコールバック
同じコードでmicrotaskへの予約を選びます。
node callback.mjs同じコードを実行 deferredmicrotaskとして予約before→after→value 3です。readValueは予約を終えると戻り、現在の同期処理の後でコールバックが実行されます。値を受け取る時点がafterより後へ移りました。
4Promiseの確定と後続処理
Promiseを作る関数内で3へ確定した後、9へ再び確定しようとします。thenは値に1を加えた新しいPromiseを返します。
cat promise.mjs確定とthenのコードnew Promiseに渡したexecutorはその場で実行されます。一方、thenに登録した処理は後で動きます。resolveを複数回呼んでも、最初に確定した結果を別の値へ変更できません。
5元のPromiseと次段の値
実行順、Promiseの同一性、二つの結果を表示します。
node promise.mjs確定・予約・次段の値を確認executorとexecutor-endが先に出ます。same?はfalseで、thenは新しいPromiseを返しています。script-endの後にthen 3が動き、次段の結果は4、元の結果は3のままです。
6awaitで関数の続きを待つ
workは最初のawaitまで進み、呼出し元へPromiseを返します。コードと結果を続けて表示します。
cat await.mjsawaitを含む関数
node await.mjs呼出し元と再開後の順序before→work-start→after true→work-resume 3→result 4です。awaitはこのasync関数の続きを待機させますが、呼出し元のafterを止めません。すでにfulfilledのPromiseでも、await後の継続は後に再開されます。
7microtaskとタイマーの順序
同期処理、Promiseのハンドラ、queueMicrotask、0ミリ秒指定のタイマーを同じスクリプトで予約します。
cat ordering.mjs予約する処理と順序
node ordering.mjs実際の表示順を確認startとendの後、この例では登録順にpromise、microtaskが動き、その後にtimerが出ます。0ミリ秒指定は「今すぐ同期的に実行」の意味ではありません。この演習は実行順を扱い、正確な経過時間を指定するものではありません。
8一つずつ開始して待つ
jobは開始を表示し、awaitで一度実行を譲ってから完了します。serialではAの結果を待った後でBを開始します。
cat sequence.mjs開始位置を変える二つの経路
node sequence.mjs serial一つずつ開始して待つstart A→done A→start B→done Bです。await job("A")の後にjob("B")を書くと、Bの開始自体がAの完了後になります。
9両方を開始してから待つ
parallel経路ではAとBを先に呼び出し、そのPromiseをまとめて待ちます。
node sequence.mjs parallel先に両方を開始して結果を待つstart A→start B→done A→done Bです。Promise.allを書くだけで処理が開始するのではなく、ここではjobを呼んだ時点でそれぞれ開始しています。開始位置を変えることで待機中に別の処理を進められます。
まとめ
コールバックが同期か非同期かは、呼出し先がいつ呼ぶかで決まります。
Promiseの確定とthenの実行は同時ではありません。thenは元とは別のPromiseを返します。
awaitを書く位置と、処理を開始する位置を分けて考えると、逐次処理と並行処理の順序を説明できます。
操作履歴と解説を振り返れます。
シェル / Node.js
cat callback.mjsconst mode = process.argv[2];
function readValue(callback) {
if (mode === "sync") callback(3);
else queueMicrotask(() => callback(3));
}
console.log("before");
readValue(value => console.log("value", value));
console.log("after");node callback.mjs syncbefore value 3 after
node callback.mjs deferredbefore after value 3
cat promise.mjsconst promise = new Promise(resolve => {
console.log("executor");
resolve(3);
resolve(9);
console.log("executor-end");
});
const next = promise.then(value => {
console.log("then", value);
return value + 1;
});
console.log("same?", promise === next);
console.log("script-end");
console.log("result", await next);
console.log("original", await promise);node promise.mjsexecutor executor-end same? false script-end then 3 result 4 original 3
cat await.mjs
node await.mjsasync function work() {
console.log("work-start");
const value = await Promise.resolve(3);
console.log("work-resume", value);
return value + 1;
}
console.log("before");
const pending = work();
console.log("after", pending instanceof Promise);
console.log("result", await pending);
before
work-start
after true
work-resume 3
result 4cat ordering.mjs
node ordering.mjsconsole.log("start");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
console.log("end");
start
end
promise
microtask
timercat sequence.mjs
node sequence.mjs serialasync function job(name) {
console.log("start", name);
await Promise.resolve();
console.log("done", name);
return name;
}
if (process.argv[2] === "serial") {
const a = await job("A");
const b = await job("B");
console.log("results", JSON.stringify([a, b]));
} else {
const a = job("A");
const b = job("B");
console.log("results", JSON.stringify(await Promise.all([a, b])));
}
start A
done A
start B
done B
results ["A","B"]node sequence.mjs parallelstart A start B done A done B results ["A","B"]
演習完了