やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【JavaScript】setIntervalでawaitをする方法

setIntervalでawaitが効かない

setIntervalPromiseのオブジェクトではないため、awaitさせて実行終了を待つことが出来ません。例えば、3秒毎にカウントを1ずつ増やしていく処理をsetIntervalで実装をすると以下のようになります。

const sleep = (ms) => setTimeout(() => undefined, ms);

const intervalCount = (ms) => {
  let count = 0;
  setInterval(() => {
      console.log("[Info] count: ", count + 1)
      count++;
  }, ms);
}


console.log("start!!");

// 1秒毎にカウント
intervalCount(1000);

// 10カウントするまで待機
sleep(10 * 1000);
console.log("finish!!");

10回分のカウントが終了するまで待機させていますが、先にstart!!finish!!のlogが表示されてしまいます。またsetIntervalに渡されたcallbackが動いたままになっている事も確認出来ます。並行に処理されているようです。

start!!
finish!!
[Info] count:  1
[Info] count:  2
[Info] count:  3
:
[Info] count:  10
[Info] count:  11
:

想定していた出力は以下でした。

start!!
[Info] count:  1
[Info] count:  2
[Info] count:  3
:
[Info] count:  10
finish!!
[Info] count:  11
:

Promiseを使って書き換える

Promiseを使うことで、先ほどのコードを想定通りの出力に変更することが出来ます。なぜPromiseを使うかというと、awaitさせる必要があるからです。
以下のようにすればオリジナルでinterval関数を作成することで、待機可能なインターバルを実装することが出来ます。インターバルと言っていますが、実際にはwhileをただ書いているだけです。

const interval = async (ms) => {
  let count = 0;
  while (count < 10) {
    await new Promise(resolve => setTimeout(resolve, ms));
    console.log("[Info] count: ", count + 1);
    count++;
  }
}

// intervalをawaitさせるためにasyncが必要なため即時関数に
(async () => {
  console.log("start!!");
  await interval(1000);

  console.log("finish!!");
})();

上記を実行すると以下のようになります。

start!!
[Info] count:  1
[Info] count:  2
[Info] count:  3
:
[Info] count:  9
[Info] count:  10
finish!!

どちらも期待通りに出力されるようになりました。考えられる拡張としてはwhileの停止条件を変えたい(eg: 10回ではなく20回にしたい)ということが考えられますが、これは引数を増やして渡すだけで簡単に対応することが出来ます。

const interval = async (ms, maxCount) => {
  let count = 0;
  while (count < maxCount) {
    await new Promise(resolve => setTimeout(resolve, ms));
    console.log("[Info] count: ", count + 1);
    count++;
  }
}

もっと複雑な条件を適応させたい場合は、条件を関数のまま渡すということも可能です。

const interval = async (ms, stopCond) => {
  let count = 0;
  while (stopCond()) {
    await new Promise(resolve => setTimeout(resolve, ms));
    console.log("[Info] count: ", count + 1);
    count++;
  }
}


// intervalをawaitさせるためにasyncが必要なため即時関数に
(async () => {
  console.log("start!!");
  // 80%の確率で停止するインターバル
  const stopCond = () => Math.random() > 0.8;
  await interval(1000, stopCond);

  console.log("finish!!");
})();

以上です。以下はおまけになります。

おまけ: インターバルの間隔を変化させる

インターバル間隔を管理する初期変数を追加して、インクリメントされるcountを利用して徐々にスリープ時間を長くするようにします。以下のコードでは静的に0.5秒ずつスリープ時間が増えていきます。

const interval = async (ms, maxCount) => {
  let count = 0;
  let intervalMs = 1000;
  while (count < maxCount) {
    const sleepMs = intervalMs + (count * 500);
    await new Promise(resolve => setTimeout(resolve, sleepMs));

    console.log("[Info] count: ", count + 1);
    count++;
  }
}

(async () => {
  console.log("start!!");
  await interval(1000, 10);

  console.log("finish!!");
})();

インターバル間隔の管理をinterval関数の内部に持たせるのが嫌な場合は外部で関数で管理させるようにすることが可能です。

const interval = async (ms, maxCount, accelerator) => {
  let count = 0;
  while (count < maxCount) {
    const sleepMs = accelerator(count);
    await new Promise(resolve => setTimeout(resolve, sleepMs));

    console.log("[Info] count: ", count + 1);
    count++;
  }
}

(async () => {
  console.log("start!!");
  const accelerator = (count) => 1000 + count * 100;
  await interval(1000, 10, accelerator);

  console.log("finish!!");
})();

acceleratorでどれぐらい加速させるかを定義しています。もちろん、減速させることも可能です。この処理を使って、ビンゴのドラムロール式のルーレットを作りました。

無料で使えるビンゴを公開しています。忘年会シーズンなどぜひご活用ください。

bingo.eni-eni.com

参考文献