一覧PG-17-1 · 非同期処理の基礎

コールバックと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

1
cat callback.mjs
const 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");
2
node callback.mjs sync
before
value 3
after
3
node callback.mjs deferred
before
after
value 3
4
cat promise.mjs
const 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);
5
node promise.mjs
executor
executor-end
same? false
script-end
then 3
result 4
original 3
6
cat await.mjs node await.mjs
async 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 4
7
cat ordering.mjs node ordering.mjs
console.log("start");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
console.log("end");
start
end
promise
microtask
timer
8
cat sequence.mjs node sequence.mjs serial
async 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"]
9
node sequence.mjs parallel
start A
start B
done A
done B
results ["A","B"]

演習完了