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に変換することで、ループが書きやすくなります。
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に変換すると簡潔に書ける