JavaScript 倒计时踩坑集锦

前阵子,项目中加了个倒计时的需求,接手的时候

复制粘贴干

啪啪啪三声,搞定,送测

let countdown = 100000; // ms 服务器返回的倒计时剩余时间

function startCountdown() {
    setTimeout({
        countdown -= 1000;
        if (/* some */) {
            // do some
            startCountdown();
        }
    }, 1000);
}
复制代码

某个彩笔开发:这波有bug我吃shi。

然后测试小姐姐反手就给了我几个 bug

哭

  • bug1: 你这东西不准啊,我看着几分钟,有好几秒的延迟
  • bug2: 你这东西有问题啊,我缩小浏览器,等一会再打开,延迟了几分钟

某个彩笔开发:不可能,我再自测一下,打你脸,我测的时候明明没问题。

五分钟后:额,额,额~emmmmmmmmm....

好嘛,我改。

原因

浏览器中的定时器任务是有误差的,也就是我们常说的 setTimeout 为什么不准的问题,这里涉及到 js 单线程以及运行机制,感兴趣的可以去了解一下,很多文章都有介绍。

在我的代码中,造成 bug 的原因:

  • 没考虑误差的叠加,也就是没有处理误差
  • 没考虑浏览器的"休眠"

处理

1. 没考虑误差的叠加,也就是没有处理误差

先看一下优化后的代码

let countdown = 100000; // ms 服务器返回的倒计时剩余时间
let countIndex = 1; // 倒计时任务执行次数
const timeout = 1000; // 触发倒计时任务的时间间隙
const startTime = new Date().getTime();

startCountdown(timeout);

function startCountdown(interval) {
  setTimeout(() => {
    const endTime = new Date().getTime();
    // 偏差值
    const deviation = endTime - (startTime + countIndex * timeout);
    console.log(`${countIndex}: 偏差${deviation}ms`);
    
    countIndex++;
    
    // 下一次倒计时
    startCountdown(timeout - deviation);
  }, interval);
}
复制代码

几乎没有什么逻辑的执行快,就已经有每秒平均 5ms 的延迟,那么 10 分钟的延迟将会累加到 3000ms。

倒计时误差是不能避免的,但是我们能尽可能的减小这个误差。

// 下次倒计时任务执行的等待时间 = 1s - 误差
startCountdown(timeout - deviation);
复制代码

这里我们通过对下一次任务的调用时间做了调整,前面延迟了多少毫秒,那么我下一个任务执行就加快多少毫秒,这就是处理倒计时误差的基本思路。

2. 没考虑浏览器的"休眠"

On most browsers inactive tabs have low priority execution and this can affect JavaScript timers.

用我蹩脚的英语翻译一下:在大多数浏览器中,待用的 tab 页优先级较低,这会对 JavaScript 的定时器造成影响。

上面的大多数浏览器不包括 ie,在 ie 下没有这个问题(ie真好用)。

蹩脚 + 概括:为了节能,对于运行在后台的 tab 页,定时器的延迟毫秒数被我们设置为 >= 1000ms。

相关链接:

针对第二点

复制上面的代码,在浏览器调试器里面试试,切换 tab 页后,明显的可以看出延迟非常明显了。

关于这一点处理,其实更多的跟业务相关,不同的业务可能有不一样的处理方式。

由我的需求引申出一个例子:

  • 网页实现一个 5 天的倒计时功能
  • 倒计时的剩余数通过请求获取,初始为432000(s),也就是5天,并且服务器端也会进行一个倒计时

我猜,目前大部分的倒计时功能都是这样实现的,前端其实只是负责一个倒计时UI的显示,能让用户感知到有这么一回事,真正倒计时的还是放在了服务器端。

前面看到了切换 tab,或者网页最小化时,有延迟,那么我们只要监听用户什么时候回到页面,这个时候再去请求服务器端最新的剩余时间,重新开始倒计时,修正造成的延迟。

目前有两种方案监听:

对于window.focus + window.blur,即使网页仍呈现在用户面前,也会触发此事件,比如从浏览器切到微信聊天(此时网页依然可见),但是仍会触发 window.blur 事件。

因此我利用 visibilitychange 事件来处理切换 tab 页以及浏览器最小化时的倒计时误差修正。

// 处理页面可见属性的改变
document.addEventListener('visibilityChange', () => {
    if (!document.hidden) {
      // get newest downtime
    }
});
复制代码

总结

在查资料的同时,好像还有几种方案也可以实现倒计时。

但是 requestAnimationFrame 好像也不能解决第二种情况:

Web Worker 过两天玩一下,还没搞过呢。

第一次写文章,写得不好请见谅,也请指出。有更好的方法也可以分享一下,感恩~!

猜你喜欢

转载自juejin.im/post/5bcd89d5e51d4579bb1c5e22