やわらかテック

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

【JavaScript】await可能なintervalの書き方

setIntervalが使いにくい

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

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

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

const main = () => {
  const intervalMs = 3000;
  console.log("start!!");

  // 3秒毎にカウント
  intervalCount(3000);

  // 10カウントするまでwait
  sleep(intervalMs * 10);

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

main();

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

start!!
finish!!
Hint: hit control+c anytime to enter REPL.
[Info] count:  0
[Info] count:  1
[Info] count:  2
[Info] count:  3
[Info] count:  4
[Info] count:  5
[Info] count:  6
[Info] count:  7
[Info] count:  8
[Info] count:  9
[Info] count:  10
[Info] count:  11
[Info] count:  12
[Info] count:  13
:
:

Promiseを使って書き換える

こちらをシリアルに実行されるようにPromiseを使ってawait可能なように書き換えました。以下のようにすれば待機可能なインターバルを実装することが出来ます。拡張性を保持するために、callbackとインターバルの継続条件(continueCondition as continueCond)は引数にて関数を受け取るようにしました。

const interval = async (ms, callback, continueCond) => {
 const _interval = async () => {
   await new Promise(resolve => {
     setTimeout(resolve, ms);
   });

   if (continueCond()) {
     callback();
     await _interval();
   }
 };
 await _interval();
}

// example
const main = async () => {
 const intervalMs = 3000;
 console.log("start!!");

 // 3秒毎にカウント
 let count = 0;
 const callback = (counter) => {
   console.log("[Info] count: ", counter);
   count++;
 };
 const continueCond = (counter) => 10 > counter;
 await interval(
   3000,
   () => callback(count),
   () => continueCond(count),
 );
 console.log("finish!!");
}

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

start!!
[Info] count:  0
[Info] count:  1
[Info] count:  2
[Info] count:  3
[Info] count:  4
[Info] count:  5
[Info] count:  6
[Info] count:  7
[Info] count:  8
[Info] count:  9
finish!!

期待通りに出力されるようになりました。拡張性を確保するために、クロージャletで定義した変数を渡しているため、ややこしくなってしまっています。 拡張性をなくして、シンプルな実装にすると非常に簡単になります。あまり使い回すような処理ではないと思うので、基本ベタ書きで良さそうです。実行結果は先ほどと同じになります。

const intervalCount = async (ms, maxCnt) => {
  let count = 0;
  const _intervalCount = async () => {
    await new Promise(resolve => {
      setTimeout(resolve, ms);
    });

    if (maxCnt > count) {
      console.log("[Info] count: ", count);
      count++;

      await _intervalCount();
    }
    
  };
  await _intervalCount();
}

// main function
const main = async () => {
  const intervalMs = 3000;
  console.log("start!!");

  await intervalCount(intervalMs, 10);
  console.log("finish!!");
}

main();

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

応用: インターバルの間隔を変化させる

(元々、これがやりたかった)
第一引数にて受け取っていたmsの固定値を関数として受け取りクロージャを利用して値を変化させるようにしました。

gist.github.com

徐々にインターバルの間隔を遅くしていく場合には、acceleratorにて値を加算させるようにします。早くしたい場合は減算させます。

const intervalMs = 1000;
let count = 0;
const accelerator = (ms, counter) => ms + (counter * 500)
const callback = (counter) => {
 console.log("[Info] count: ", counter);
 count++;
};
const continueCond = (counter) => 10 > counter;
await interval(
 () => accelerator(intervalMs, count),
 () => callback(count),
 () => continueCond(count),
);
console.log("finish!!");
start!!
sleepTime:  1000
[Info] count:  0
sleepTime:  1500
[Info] count:  1
sleepTime:  2000
[Info] count:  2
sleepTime:  2500
[Info] count:  3
sleepTime:  3000
:
:

React.jsでランダム値を出力するcomponentを作るのに使いました。 f:id:takamizawa46:20210103161915g:plain

参考文献