2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

setTimeout(…, 0) って何の意味があるの?

Posted at

概要

await new Promise(resolve => setTimeout(resolve,0));

そのまま読めば「0ミリ秒待つ」。
何の影響も与えなそうに見えますが、そんなことはないです。
(実際にはわずかに処理遅延しますが、そういう意味ではありません。)
どんな意味があるのか、この1行の有無で動作が変わる例を挙げて解説します。

関連ワード

  • イベントループ(Event loop)
  • タスクキュー
  • マイクロタスク
  • Promise, async/await

簡単に言うと

マイクロタスクキューに溜まっているイベントの処理を全部消化してから戻ってくるよ!

大げさな例ですが、このような状況を考えます。

以下の実装があるとします。

sample.js
const eventTarget = new EventTarget();
eventTarget.addEventListener("myEvent", async () => {
  console.log("--- start event ---");
  await takeTime(); // 時間のかかる処理
  console.log("--- finish event ---");
});

console.log("=== start ===");
eventTarget.dispatchEvent(new Event("myEvent"));
console.log(`=== finish ===`);

イベント(myEvent)を発火させると、時間のかかる処理が非同期で走りますね。
ちなみに、takeTime() の実装は何でもよいのですが、以下としました。

async function takeTime(){
  // 0~10秒ランダムに待つ
  await new Promise((resolve) => setTimeout(resolve, Math.random() * 10000));
}

ここで、そのまま実行するとご覧の通り、メインプログラムの終了時点ではイベントの処理は終わっていません。

$ node sample.js
=== start ===
--- start event ---
=== finish  ===
--- finish event ---

ここで、確実にイベントの処理が終わってから、メインプログラムを終了させたいケースが発生したとします。
eventのリスナー関数をawaitすることはできませんので、別の方法を考える必要があります。

今回は takeTime() が最大10秒だとわかっていますが、そうではない場合もあるため、
ここでは(雑ですが)このように実装することにしました。

sample.js(実行はお勧めしません)
let eventFinished = false; // フラグを追加
const eventTarget = new EventTarget();
eventTarget.addEventListener("myEvent", async () => {
  console.log("--- start event ---");
  await takeTime(); // 時間のかかる処理
  eventFinished = true; // フラグを立てる
  console.log("--- finish event ---");
});

console.log("=== start ===");
eventTarget.dispatchEvent(new Event("myEvent"));
while (!eventFinished) {
  // 何もせずひたすら待つ
}
console.log(`=== finish ===`);

こちら、なんとなくうまく動きそうですが、いつまで待っても 実行終了しません。
CPUに負荷がかかるだけなのでやめましょう。
(whileループの中に別の同期処理を入れても結果は同じです。)

$ node sample.js
=== start ===
--- start event ---
※ここから先動かないので、強制終了

理由はいったん置いておいて、ここに1行加えるとうまく動きます。

sample.js
let eventFinished = false; // フラグを追加
const eventTarget = new EventTarget();
eventTarget.addEventListener("myEvent", async () => {
  console.log("--- start event ---");
  await takeTime(); // 時間のかかる処理
  eventFinished = true; // フラグを立てる
  console.log("--- finish event ---");
});

console.log("=== start ===");
eventTarget.dispatchEvent(new Event("myEvent"));
while (!eventFinished) {
  await new Promise((resolve) => setTimeout(resolve, 0)); // 0ms待つ
}
console.log(`=== finish ===`);
$ node sample.js
=== start ===
--- start event ---
--- finish event ---
=== finish  ===

なぜ、「0ミリ秒待つ」を挟むだけで動くのでしょうか?

大雑把な解説

大前提として、JavaScriptはシングルスレッドで動いています。
並行に動いて見える処理も、同じスレッドで、タスクに順番をつけて行っているだけです。

この順番付けに使っているのが、「タスクキュー」 「マイクロタスクキュー」 の2つです。
詳しくは本記事では解説しませんが、簡単に考えるなら
「プログラム全体の順番を司るもの」と「割り込み処理を司るもの」だと思って大丈夫です。

基本的に、プログラムは順次実行されるので、タスクを「タスクキュー」に入れて処理しますが、
イベントなどの(いわゆる “割り込み” の)処理は、「マイクロタスク」として「マイクロタスクキュー」に入ります。

タスクキュー内のタスクが1つ終わったら、
マイクロタスクキュー内の処理をすべて終えて、
またタスクキュー内のタスクを1つ終える……
このループ(イベントループ と呼びます)を繰り返して、並行処理を実現しているのです。

今回の例では、
takeTime() を完了させるという命令(イベント)がマイクロタスクキューに入っているが、
 メインのタスク(whileループ)の区切りがつかないため、takeTime()も完了できない」
という状態に陥っていたのです。

ここで、例の1行

await new Promise(resolve => setTimeout(resolve,0));

を加えることで、whileループを区切ることができます。
(より正しく言うと、1ループごとにawaitすることで、毎ループの処理をマイクロタスクキューに入れています)
このおかげで、1ループごとにマイクロタスクキュー内のタスクが処理されるため、
takeTime() の完了処理が実行されるということです。

最後に

Promise、async/await系は記事が多いのですが、
イベントループについてはちょうどよくまとまっている記事が少なく、
この情報にたどり着きにくかったので、書きました。
記事の内容に間違いがあったら申し訳ありません。適宜修正します。
もっと詳しく正確な情報を知りたい人は「イベントループ」で検索するとよいと思います。
加えて「Promise」「async, await」あたりがさらなる参考になるかもしれません。
この記事が皆さんの一助となれば幸いです。

2
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?