setInterval
делает все возможное, чтобы пространство выполнения обратного вызова соответствовало заданному интервалу. Дело в том, что в игре вам действительно нужно, чтобы текущее состояние мира отображалось на экране плавно и своевременно. Это отличается от поведения setInterval
, который ничего не знает об экране и неоднократно вызывается вслепую.
Например: если вы запускаете setInterval(foo, 100)
для своей игры на вкладке браузера, а затем перейдите на другую вкладку в вашем браузере, подождите десять секунд, а затем вернитесь в свою игру, ваш foo
обратный вызов будет вызываться около ста раз подряд , поскольку поставленные в очередь обратные вызовы "истощаются". Маловероятно, что вам нужно такое поведение.
requestAnimationFrame
является лучшим решением для этого, потому что оно вызывается только тогда, когда (незадолго до этого) ваша игра рендерится - то, что вы want.
В следующем коде объект таймера создается createTimer
. Таймер имеет методы start
, stop
и toggle
.
Метод start
записывает, когда он был вызван, и запускает requestAnimationFrame
, предоставляя обратный вызов с именем tick
. Каждый раз, когда происходит тик, мы запускаем некоторый logi c, чтобы увидеть, какой (если таковой имеется) обратный вызов вызывать.
Если истекшее время больше или равно duration
таймера, то onTimeout
обратный вызов вызывается и таймер останавливается.
Если прошедшее время меньше, чем duration
, но больше или равно периоду interval
, то мы обновляем lastInterval
и вызовите обратный вызов onInterval
.
В противном случае мы просто включим еще один тик таймера.
Метод stop
просто использует идентификатор анимации запроса для отмены таймера с помощью cancelAnimationFrame
.
function createTimer() {
let rafId = null
function start({duration = 10000, interval = 1000, onInterval, onTimeout, onStop, startTime=performance.now(), lastInterval = startTime}) {
function tick(now=performance.now()) {
const elapsed = now - startTime
if (elapsed >= duration) {
cancelAnimationFrame(rafId)
rafId = null
return onTimeout()
}
if ((now - lastInterval) >= interval) {
lastInterval = now
onInterval({
duration,
elapsed
})
}
rafId = requestAnimationFrame(tick)
}
rafId = requestAnimationFrame(tick)
}
function stop() {
cancelAnimationFrame(rafId)
rafId = null
return onStop()
}
function toggle(...args) {
rafId ? stop() : start(...args)
}
const timer = {
start,
stop,
toggle
}
return timer
}
const timer = createTimer()
const onInterval = ({duration, elapsed})=>console.log(`Remaining: ${((duration - elapsed)/1000).toFixed(0)}`)
const onTimeout = ()=>console.log('Timed out.')
const onStop = ()=>console.log('Manually stopped.')
document.getElementById('btn').addEventListener('click', () => timer.toggle({
onInterval,
onTimeout,
onStop
}))
<button id="btn">Toggle Timer</button>