+ 作るログイン
図を読み込んでいます…
1 / 7
動きでわかる Webとデータの仕組み / 全10章

イベントループ:Promiseとタイマー、どちらが先?

発行
アニメモ公式
版
第1版
改訂
コマ
全7

ブラウザでA・D・B・Cと出力される小さなプログラムを、現在のタスク、マイクロタスク、タイマーに分けて追う。

書いた順と、コールバックの実行順は違う

ブラウザの通常のスクリプトとして、次のコードだけを実行するとします。タイマーのコールバックが実行できるまで、ページは開いたままとします。

javascript
console.log('A');
setTimeout(() => console.log('C'), 0);
Promise.resolve().then(() => console.log('B'));
console.log('D');

答えは A → D → B → C です。現在の処理、マイクロタスク、タイマーのタスクを分けると、その理由を追えます。Node.js特有のキューや、モジュールの評価順を説明する図ではありません。HTML Standard:イベントループ

  • 08 / WEB SYSTEMS
  • 1 / 7
  • この順に書いたら、何の順で表示される?
  • console.log('A');
  • setTimeout(() => console.log('C'), 0);
  • Promise.resolve().then(() => console.log('B'));
  • console.log('D');
  • 出力:?
  • ブラウザの通常のスクリプト一回分を、順番に追う。

タイマーを登録しても、今の処理は止まらない

最初の console.log でAが出ます。次のsetTimeoutは、条件が整った後にコールバックをタスクとして実行するための予約です。そこでCを即座に実行するわけではありません。

0 は、今のJavaScript処理を中断して割り込む指示ではなく、指定した遅延を経た後の実行を求める値です。実際の時刻には、ほかの処理やブラウザの制限も関わります。

図のタイマー欄は「待っている仕事」をまとめた説明です。まだ実行可能になる前のタイマー待ちと、実行可能になったタスクの段階を、本文で区別しています。

  • 08 / WEB SYSTEMS
  • 2 / 7
  • まずAを実行。タイマーは後の処理として登録
  • 実行中の処理
  • マイクロタスク
  • タイマーのタスク
  • 現在のタスク
  • Promiseの反応など
  • 実行可能になってから待つ
  • 同期処理を続ける
  • C:タイマー待ち
  • 出力:A
  • setTimeoutの0は、今の処理へ割り込む指定ではない。

thenを予約し、Dまで同期処理を続ける

Promise.resolve() が返すPromiseは既に履行済みですが、thenに渡したコールバックを、この行で同期的に呼び出すわけではありません。Promiseの反応をマイクロタスクとして予約します。

現在の処理は続き、最後の console.log('D') に到達します。ここまでの出力はA、Dです。BもCも、今のコードへ割り込んでいません。

「Promiseに関連するコードはすべて後で動く」と広げないようにします。例えばPromiseのコンストラクターに渡すexecutorは同期実行ですが、このサンプルには登場しません。MDN:マイクロタスク

  • 08 / WEB SYSTEMS
  • 3 / 7
  • Promiseの反応を予約し、同期処理Dを続ける
  • 実行中の処理
  • マイクロタスク
  • タイマーのタスク
  • 現在のタスク
  • Promiseの反応など
  • 実行可能になってから待つ
  • Dを実行
  • B:thenの処理
  • C:タイマー待ち
  • 出力:A → D
  • thenのコールバックは、今の同期処理を中断しない。

現在の処理の後に、マイクロタスクを取り出す

同期処理が終わり、マイクロタスクのチェックポイントへ進みます。キューにあるPromiseの反応が実行され、Bが表示されます。出力はA、D、Bになりました。

この場面では、実行可能なタイマーがあっても、次のタスクへ移る前にマイクロタスクを処理します。図の紫の札が「実行中」の欄へ動くのは、その切り替わりです。

ブラウザの仕様ではチェックポイントが行われる場所は他にもあります。これは、この短いスクリプトでの順序を理解するための基本的な経路です。HTML Standard:マイクロタスクのチェックポイント

  • 08 / WEB SYSTEMS
  • 4 / 7
  • 同期処理が終わり、マイクロタスクを実行
  • 実行中の処理
  • マイクロタスク
  • タイマーのタスク
  • 現在のタスク
  • Promiseの反応など
  • 実行可能になってから待つ
  • Bを実行
  • C:まだ待つ
  • 出力:A → D → B
  • 次のタスクの前に、マイクロタスクのチェックポイントがある。

その後で、実行可能なタイマーの仕事へ進む

マイクロタスクの処理が終わると、イベントループは次の仕事を選べます。タイマーが実行可能になり、そのタスクが選ばれるとCが表示されます。これでA、D、B、Cが揃います。

ブラウザには入力、通信などの仕事もあり、全種類の仕事が一つのFIFOキューに並んでいると断定はできません。この例の4文字の順序を、そのままあらゆる非同期APIの順序表へ拡張しないようにします。

描画にも機会がありますが、タスクのたびに必ず一回画面を描くという保証ではありません。フレーム更新の必要性やブラウザの状態も関わります。

  • 08 / WEB SYSTEMS
  • 5 / 7
  • 実行可能なタイマーのタスクへ進む
  • 実行中の処理
  • マイクロタスク
  • タイマーのタスク
  • 現在のタスク
  • Promiseの反応など
  • 実行可能になってから待つ
  • Cを実行
  • 出力:A → D → B → C
  • この例の結果は A → D → B → C。0msは正確な時刻ではない。

処理中に増えたマイクロタスクも続けて扱う

別の例として、Bの処理がさらにB2をマイクロタスクへ追加したとします。チェックポイントは、キューが空になるまで取り出して処理します。チェックポイント開始時にあった一個だけで打ち切るわけではありません。

そのため、自分を無限に追加するマイクロタスクは、次のタスクや描画の機会を妨げます。小さな非同期処理へ分けたつもりでも、利用者からは固まって見えることがあります。

後で動かすことは、他の仕事へ必ず公平に時間を渡すことではないのです。MDN:マイクロタスク使用上の注意

  • 08 / WEB SYSTEMS
  • 6 / 7
  • 追加のマイクロタスクも、空になるまで続く
  • チェックポイント
  • キューが空になるまで取り出す
  • B
  • 追加されたB2
  • …
  • 出力:追加が続くと、他の仕事が待つ
  • 無限に追加すると、次のタスクや描画の機会を妨げる。

Promiseに入れれば、重い計算は並列になる?

答えはいいえです。通常のthenのコールバック内で長い計算を行えば、それを実行している間、同じ実行環境の他の仕事は待ちます。Promiseは別スレッドで計算を始める命令ではありません。

別の実行場所を必要とするCPU処理なら、Web Workerなどが検討対象です。仕事をどこで実行するかと、いつ実行するかを分けて考えます。

この作品のコードに戻れば、同期のAとD、マイクロタスクのB、タイマーのCです。札を三つの欄に分けてから動かすと、暗記よりも実行順を説明しやすくなります。

  • 08 / WEB SYSTEMS
  • 7 / 7
  • 確認:Promiseなら、重い計算も別スレッド?
  • 実行する順番
  • 別の実行場所
  • Promise・タイマーで予約
  • Web Workerなどを検討
  • then内の重い計算も時間を使う
  • 答えは NO。非同期に予約することと、並列実行は別の話。

0 件のコメント

読み込み中…

権利侵害・不適切な内容の連絡: [email protected]