2022-07-03 周报

使用 JavaScript 实现倒计时、JS 定时器注意事项

1. JS 倒计时

agenda 插件需要增加番茄钟功能，在 github 上找到 usePomodoro ，看起来功能挺全的，而且使用 hook 使用起来很方便，虽然 star 数量并不多，但是这个功能也不复杂，如果有问题我自己能 hold 住，所以最终就选了它。

但是在功能开发完上线后才发现，总是计时不准确，明明设置的 25min 番茄钟，结果一个小时过去了，倒计时也没跑完。

后来拿着手机对比研究一下，确定了当软件处于后台时就会出现误差。突然想起来以前也遇见过类似的情况，当浏览器 tab 处于非激活的状态时，定时器就可能不准确。

去仓库看了下代码，果然问题出在这里：

Copy
// 原代码使用 setInterval，来计算倒计时剩余时长
setInterval(tick, 1000)

当 tab 不处于激活状态（tab 被隐藏，浏览器处于后台）时，setInterval 的触发间隔就并不是设定的 1000ms。这可能是浏览器出于性能与耗电等考虑设计的机制。

那么如何处理呢？

既然 js 不能保证代码每隔 1s 执行一次，就只能从外部下手。比较普遍的方法是使用系统时间作为参照物：

Copy
function countDown(length) {
const start = new Date().valueOf()
const timer = setInterval(() => {
const now = new Date().valueOf()
const past = now - start
if (past <= 0) {
console.log('count down end')
clearInterval(timer)
}
}, 1000)
}

知道开始时间及倒计时的时长后，就能算出结束时间，也能在每次定时器回调中知道已经过去的时间。将计时的任务交给系统，js 只是轮询查流逝的时间，这样就可以避免误差。

这个是我提的 pr。

这个方法也有一个问题，如果用户一直让页面处于非激活状态，在倒计时应该结束的那一刻，setInterval 可能也无法执行，需要到用户激活 tab，才能再次触发。

如果需要解决这个问题，感觉一个方案是包装成 app 来进行后台保活，一个方案是把状态信息存到服务器，当计时结束时服务器给 js 发消息。

2. js 定时器注意事项

基础使用很简单，我们这里只说需要注意的几个点。

2.1 定时器嵌套

setInterval 回调可以嵌入对另一个定时器的调用，为了减轻对性能的影响，一旦定时器嵌套超过 5 层，则浏览器将强制设置定时器最小时间间隔为 4ms。

2.2 确保代码执行时间短于定时器间隔

定时器回调属于异步任务，只有调用定时器的主线程其他代码执行完毕才会执行回调函数。如果前面的代码执行时间超过定时器间隔，那么定时器就不会在正确的时间触发。所以浏览器无法保证一定在正确的时间触发回调。

2.3 未被激活的 tabs 的定时最小延迟 ≥ 1000ms

为优化后台 tab 的加载损耗（以及降低耗电量），在未被激活的 tab 中定时器最小延迟时间为 1000ms。

2.4 最大延迟值

浏览器以 32 位带符号整数存储延时，当延时大于 2147483647 毫秒 (大约 24.8 天) 时就会溢出，此时定时器会立即执行。

参考文章

window.setTimeout - Web API 接口参考 | MDN

setInterval () - Web API 接口参考 | MDN