setTimeout()は関数を一度だけ後で実行し、setInterval()は一定間隔で繰り返し実行します。指定時間は「その時刻ぴったりに実行する保証」ではなく、実行可能になるまでの最小待ち時間です。不要になったタイマーはIDを保持してclearTimeout()またはclearInterval()で解除します。
この記事はブラウザのタイマーの選択、解除、遅延への対処を扱います。Promiseの順序制御はasync/await、キューの実行順はイベントループに分けています。
setTimeoutとsetIntervalはどう違うか
| API | 実行回数 | 停止方法 | 向く用途 |
|---|---|---|---|
setTimeout() |
原則1回 | clearTimeout(id) |
遅延処理、一定時間後の通知、再試行 |
setInterval() |
解除するまで繰り返す | clearInterval(id) |
短く一定な定期確認 |
再帰的setTimeout() |
前回完了後に次回を予約 | 次回IDを解除、予約しない | 処理時間が変動するポーリング |
requestAnimationFrame() |
描画更新前 | cancelAnimationFrame(id) |
視覚アニメーション |
setTimeoutを一度だけ実行する方法
setTimeout(コールバック関数, 遅延ミリ秒)と書くと、指定した時間が過ぎてから関数が一度だけ呼ばれます。第3引数以降は、そのままコールバック関数の引数として渡されます。戻り値のタイマーIDを保存しておくと、実行前に中止できます。
関数を「呼ばずに渡す」のがポイントで、setTimeout(fn(), 1000)と括弧を付けると即時に実行され、戻り値がタイマーに渡ってしまいます。関数を値として渡す考え方は第一級関数と高階関数で整理しています。
const timeoutId = setTimeout((message) => {
console.log(message);
}, 1000, "1秒以上たってから実行");
const shouldCancel = false;
if (shouldCancel) {
clearTimeout(timeoutId);
}
コールバックを文字列で渡す形式は、任意コード実行やデバッグ困難につながるため使いません。関数参照またはアロー関数を渡します。
setIntervalを繰り返して解除する方法
setInterval(コールバック関数, 間隔ミリ秒)と書くと、指定した間隔ごとに関数が呼ばれ続けます。止める方法は一つで、戻り値のIDをclearInterval(id)に渡します。ページを離れても自動では止まらないため、終了条件を必ず用意し、条件を満たしたら解除します。画面やコンポーネントを破棄したときにも解除できるよう、IDを必要なスコープで保持します。
let count = 0;
const intervalId = setInterval(() => {
count += 1;
console.log(count);
if (count >= 3) {
clearInterval(intervalId);
}
}, 1000);
解除し忘れると、画面から対象が消えた後も処理が続き、不要な通信やエラーの原因になります。初期化処理とcleanup処理を対にしてください。
オブジェクトのメソッドをそのまま渡すと、呼び出し時のthisが失われます。setInterval(obj.update.bind(obj), 1000)のように束縛するか、アロー関数で包みます。詳しくはbind()・call()・apply()の使い方を参照してください。
指定した時間より遅れるのはなぜか
タイマーの遅延値は、コールバックをキューへ進められる最短の目安です。現在実行中のJavaScript、他のタスク、ブラウザのバックグラウンドタブ制限などにより、実行は遅くなる場合があります。
setTimeout(() => {
console.log("later");
}, 0);
console.log("now");
// now
// later
0ミリ秒でも即時実行ではありません。現在の同期処理が完了し、イベントループがコールバックを実行できる段階になってから動きます。
処理の重なりを避けるにはどうするか
interval間隔よりコールバック処理が長い場合、次の実行が詰まり、意図した周期になりません。通信など完了時間が変動する処理は、完了後に次のsetTimeout()を予約します。
let stopped = false;
let timeoutId;
async function poll() {
try {
await refreshStatus();
} catch (error) {
console.error(error);
} finally {
if (!stopped) {
timeoutId = setTimeout(poll, 5000);
}
}
}
function stopPolling() {
stopped = true;
clearTimeout(timeoutId);
}
poll();
この方式は「前回処理の完了から5秒後」に次を始めます。絶対時刻に合わせる必要がある時計やカウントダウンでは、実行回数を数えるだけでなくDate.now()などから残り時間を毎回再計算します。
アニメーションにsetIntervalを使うべきか
画面上の動きをフレームごとに更新するなら、通常はrequestAnimationFrame()を使います。ブラウザの再描画タイミングへ合わせやすく、非表示タブでの動作も描画用途に適した扱いになります。
let animationId;
function update(timestamp) {
renderFrame(timestamp);
animationId = requestAnimationFrame(update);
}
animationId = requestAnimationFrame(update);
function stopAnimation() {
cancelAnimationFrame(animationId);
}
タイマーは経過後の処理、requestAnimationFrame()は表示更新、と目的で分けます。
タイマーが止まらないときの確認手順
- 戻り値のIDを変数へ保存しているか確認する。
- 開始APIと解除APIの組み合わせを確認する。
- 同じ初期化処理が複数回呼ばれ、複数タイマーが作られていないか確認する。
- 終了条件と画面破棄時のcleanupが実行されるか確認する。
- 正確な時刻が必要なら、回数ではなく現在時刻から再計算する。
一度だけならsetTimeout()、短い定期処理ならsetInterval()、完了時間が変わる処理なら再帰的setTimeout()、描画ならrequestAnimationFrame()を選びます。処理の中でPromiseを待つ場合はasync/awaitのエラー処理も合わせて確認してください。