requestAnimationFrameのループはPromiseに変換すると簡潔に書ける(かも)


requestAnimationFrame()は、フレームレートを考慮してコールバック関数を実行する関数です。主にcanvasなどのアニメーションで使います。

このrequestAnimationFrameですが、コールバックに渡した関数は1度だけ実行されます。

globalThis.requestAnimationFrame(()=>{
  // この部分は1度だけ実行される
  console.log("raf!");
});

これをループさせて、複数回実行させるためには、例えばこのサイトでは以下のような方法が紹介されています。

const startTime = Date.now(); //描画開始時刻を取得
(function loop(){
  globalThis.requestAnimationFrame(loop);
  console.log(startTime - Date.now()); //経過時刻を取得
})();

名前付きの即時関数を使って実装されているのですが、ループしていることが一目では分かりづらくなっています。
ループを抜けるための条件分岐が足されると、更に読みづらくなってしまいそうです。

ループを簡潔に書くためにはどうしたらよいでしょうか。

requestAnimationFrameをPromiseに変換する

requestAnimationFrameをPromiseに変換することで、ループが書きやすくなります。

requestAnimationFrameをPromiseに変換
function animationFramePromise() {
  return new Promise<number>((resolve) => {
    globalThis.requestAnimationFrame(resolve);
  });
}

Promiseに変換した後は、ループを以下のように書くことができます。

const startTime = Date.now(); //描画開始時刻を取得
while (true) {
  await animationFramePromise(); // この行で、次の更新タイミングまで待つ
  console.log(startTime - Date.now()); //経過時刻を取得
}

while文を使うことで、ループであることが一目で分かるようになりました。

ループを抜ける条件を書く時は、while文のbreak条件の部分に指定するか、if文でbreakするだけです。

const startTime = Date.now();
while (startTime - Date.now() < 10000) { // 終了条件
  await animationFramePromise();
  console.log(startTime - Date.now());

  // 終了条件はこう書くこともできる
  // if (startTime - Date.now() > 10000) {
  //   break;
  // }
}

setTimeoutにも応用できる

ちなみにこの方法、setTimeoutやqueueMicrotaskにも応用できます。

function delay(ms) {
  return new Promise<number>((resolve) => {
    globalThis.setTimeout(resolve, ms);
  });
}

const startTime = Date.now(); //描画開始時刻を取得
while (true) {
  await delay(1000);
  console.log(startTime - Date.now()); //経過時刻を取得
}

setTimeoutをPromise化する際の注意点として、時間計測のスタート地点が「前の処理が終了した時刻」になります。そのため、例えば1000ミリ秒を指定してもぴったり1000ミリ秒間隔になるわけではありません。

GUIの操作など、「ぴったりn秒」が要求されない箇所では、setIntervalよりこちらのほうが書きやすい事もあるかもしれません。

まとめ

  • requestAnimationFrameを使ったループ処理はPromiseに変換すると簡潔に書ける
  • setTimeoutやqueueMicrotaskも同様にPromiseに変換すると簡潔に書ける