在日常需求中,我们经常会需要写倒计时功能。但有时会发现快慢不一的情况,那究竟是怎么回事呢?我们先带着疑问想一下,写倒计时功能应该用setInterval还是setTimeout?
- 假设我们使用setInterval,我们可能这样写
let interval = 1000let countdown = () => {
// do someting...
}countdown()setInterval(countdown, interval)
我们可能以为它的执行顺序是这样
文章图片
现在我把它的执行时常变得大于它的interval,再来看一下
文章图片
我们发现执行完回调后并没有等待一秒再继续执行,而是在回调执行完后立即又开始执行
这是因为setInterval会在interval时间到达后,看下是否有回调正在执行,如果有,则等待下个周期,不管回调是执行800毫秒还是2000毫秒,只要interval时间到了,都会尝试往事件队列插入【为什么你写的倒计时总是有误差】2.假设我们使用setTimeout,我们可能会这样写
所以我们要写倒计时,最好不要在回调里放入过多逻辑,回调执行时间不能大于interval,否则两次执行将没有间隔
let interval = 1000countdown(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) returncurIdx++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毫秒左右,不会出现时间越来越大的情况。参考文章:window.setTimeout - Web API 接口参考 | MDN
另外还有两个需要注意的点:
1. 由于js引擎对于加载损耗的优化策略,页面在显示和隐藏时会有更多的误差,我们可以在显示时重新执行
- timeStemp最好从服务器端取
https://segmentfault.com/a/1190000040397253
David Baron's weblog: setTimeout with a shorter delay
?
推荐阅读
- 首屏时间,你说你优化了,那你倒是计算出给给我看啊!
- 选择篇(021)-下面代码的输出是什么?
- 精读《zustand 源码》
- 浅谈js的垃圾回收机制
- 选择篇(017)-哪个选项是不正确的?
- JavaScript笔记之如何写好JavaScript
- 拉新×23,盈利可能性高出19倍,嵌入式分析到底有多香
- 厉害了,JavaScript 新提案(array.groupBy())
- 【JavaScript Weekly #554】ES2022 特性(类静态初始化块)
- 单元测试(即刻搞定!)