setInterval
は手軽ですが、それが故に正確な時間を必要とする場合には、あまり役に立たなくなります。
手軽に時計を作ってみる
setInterval(関数, 1000);
のように書けば、関数
を1秒毎に実行することとなります。ここで描画処理を入れれば、秒単位で表示する時計の一丁上がりです。
…が、このままではうまくいかない箇所があります。
- スタートのタイミングの問題…
setInterval
をスタートする瞬間がちょうど秒の変わる瞬間だったという幸運なパターンもありえますが、最悪の場合1秒近く遅れることとなります。 - 1秒より間隔が長くなる…
setInterval
の時間は、「前の実行が終わってから次の実行が始まるまでに、最低限確保される待ち時間」ですので、実行同士の間隔は関数自体の実行時間が足し込まれて、必ずセットした時間を上回ることになります。また、他のコードの実行で遅れることもありえますし、ブラウザ側で意図的に(消費電力節約のためなどで)それ以上待たせてもいいことになっています(W3C HTML 5.2)。 - 上の2つが重なれば、「遅れがそこまででもないのに秒が飛ぶ」ことも考えられます。1回めを0.999秒で実行開始したとして、タイマーが2ミリ秒余分にかかれば、次の実行は2.001秒となって、1秒台の実行がなくなってしまいます1。
このような事情がありますので、「できるだけ安定したサイクルを保ちたい」あるいは「秒が変わった瞬間に実行したい」という場合、setInterval
はあまり適当な選択肢ではありません。
setTimeout
を使う
では、代わりにsetTimeout
を使ってみましょう。setTimeout
でループ処理をさせるときの基本形は、以下のような形です。タイマーで呼ばれた関数から、さらに同じ関数をsetTimeout
する、という流れです。
setTimeout(function main(){
// メインの処理
setTimeout(main, 時間);
}, 時間);
あとは時間
をどう設定するかだけですが、Date.now()
で現在のミリ秒が得られます。「ちょうど秒が変わる瞬間」はシリアル値が1000で割り切れますので、1000 - Date.now() % 1000
のようにすれば、そこまでの待ち時間を導けます。
毎回待ち時間を正しく設定することで、秒の変わり目にできるだけ近いところでコンスタントに関数を実行できますし、処理の遅延が1秒以上にならない限り、秒が飛ぶことも起きません。
Codepen上に作ってみました
リロードなどでタイミングを調節すれば、setInterval
のほうが遅れている様子もわかるかと思います。
(なぜかReactで作ってしまったので、ちょっと見づらいかもしれません)
See the Pen pYGKgR by Jkr2255 (@jkr2255) on CodePen.
-
もちろん、ブラウザ処理の重さなどで、実行が遅れうることは予め想定して実装が必要なのは間違いないです。 ↩