乐趣区

关于前端:为什么你写的倒计时总是有误差

 在日常需要中,咱们常常会须要写倒计时性能。但有时会发现快慢不一的状况,那到底是怎么回事呢?

咱们先带着疑难想一下,写倒计时性能应该用 setInterval 还是 setTimeout?

  1. 假如咱们应用 setInterval,咱们可能这样写
let interval = 1000

let countdown = () => {// do someting...}

countdown()

setInterval(countdown, interval)

咱们可能认为它的执行程序是这样

当初我把它的执行时常变得大于它的 interval,再来看一下

 咱们发现执行完回调后并没有期待一秒再继续执行,而是在回调执行完后立刻又开始执行

这是因为 setInterval 会在 interval 工夫达到后,看下是否有回调正在执行,如果有,则期待下个周期,不论回调是执行 800 毫秒还是 2000 毫秒,只有 interval 工夫到了,都会尝试往事件队列插入

所以咱们要写倒计时,最好不要在回调里放入过多逻辑,回调执行工夫不能大于 interval,否则两次执行将没有距离

2. 假如咱们应用 setTimeout,咱们可能会这样写

let interval = 1000

countdown(interval)

const countdown = (interval) => {let timer = setTimeout(() => {clearTimeout(timer)

        // do someting...
    
        countdown(interval)

    }, interval)

}

 应用 setTimeout 咱们便能够保障逻辑解决有精确的距离,不会呈现下面的状况,但咱们又会发现另一个问题

 通过试验,咱们会看到每次打印的距离时长会越来越长,其实除了 js 的执行工夫还有一个造成误差的中央,咱们看下官网形容

 另外 mdn 还举荐了如何实现 0 毫秒提早的定时器的办法,我也放在这里

David Baron’s weblog: setTimeout with a shorter delay!

那么如何防止这种误差呢?咱们能够通过计算来解决这个问题,附上残缺代码

/**
 * 倒计时工具
 * @param {object} option -  配置项
 * @param {number} option.timeStemp - 工夫戳,毫秒
 * @param {number} [option.interval=1000] - 间隔时间, 默认 1000 毫秒
 * @param {Function} [option.callback] - 回调
 * @returns {Function} cancel 进行计时器
 * 注(肯定留神):不论是应用此工具还是本人写,回调的整体执行工夫都不应该超过 interval
 */

const countdownTool = function ({timeStemp, interval: originalInterval = 1000, callback = (resetTime) => void 0,
}) {if (!timeStemp || timeStemp <= 0 || timeStemp < originalInterval) return callback(0)

    let stop = false
    const cancel = () => {stop = true}

    let curIdx = 1
    let interval = originalInterval
    let ct = Date.now()
    countdown(interval)

    function countdown (interval) {if (stop) return
        let timer = setTimeout(function () {clearTimeout(timer)

            let resetTime = timeStemp - originalInterval * curIdx
            if (resetTime < 0) resetTime = 0
            callback(resetTime)
            if (!resetTime) return

            curIdx++

            let ct2 = Date.now()
            let deviation = ct2 - interval - ct
            if (deviation >= originalInterval || deviation <= 0) deviation = 5 // 避免歹意更改本地工夫
            ct = Date.now()
            countdown(originalInterval - deviation - (ct - ct2))
        }, interval)
    }

    return cancel
}

在控制台执行下

 能够看到,每次回调里拿到的工夫距离都在 1000 毫秒左右,不会呈现工夫越来越大的状况。

另外还有两个须要留神的点:

1. 因为 js 引擎对于加载损耗的优化策略,页面在显示和暗藏时会有更多的误差,咱们能够在显示时从新执行

  1. timeStemp 最好从服务器端取

参考文章:window.setTimeout – Web API 接口参考 | MDN

                  https://segmentfault.com/a/1190000040397253

                  David Baron’s weblog: setTimeout with a shorter delay

退出移动版