倒计时为什么会越走越慢:别用 setInterval 累减,改用时间戳
【摘要】 番茄钟切到后台后倒计时明显变慢。本文分析 setInterval 累减的三个误差来源(后台节流、任务堆积、误差累积),给出用结束时间戳现算剩余时间的写法,并说明暂停恢复、取整方式与 performance.now 的取舍。
番茄钟上线后我发现一个问题:把标签页切到后台去写作业,25 分钟的专注结束时,手机上的真实时间已经过去快 30 分钟了。代码逻辑没错,错在计时方式。
一、错误写法:每秒减一
let left = 25 * 60;
setInterval(() => {
left--; // 假设每次刚好过去 1 秒
render(left);
}, 1000);
这段代码隐含了一个假设:setInterval 每次回调之间刚好间隔 1000 毫秒。实际上这个假设在三种情况下都不成立。
- 后台节流:标签页不可见时,浏览器会把定时器的最小间隔拉长到 1 秒以上,移动端甚至可能直接冻结;
- 任务堆积:主线程忙于渲染或计算时,定时器回调会被推迟,但不会补偿;
- 误差累积:每次哪怕只慢 5 毫秒,1500 次之后就差了 7 秒多。
后台节流是其中影响最大的。切走几分钟,倒计时可能只走了几十秒。
二、正确写法:记录结束时刻
把“还剩多少秒”从一个需要自己维护的变量,改成每次用系统时间算出来的结果:
let endAt = Date.now() + 25 * 60 * 1000;
const timer = setInterval(() => {
const left = Math.max(0, Math.round((endAt - Date.now()) / 1000));
render(left);
if (left === 0) { clearInterval(timer); onFinish(); }
}, 250); // 刷新频率只影响画面,不影响计时
这样定时器的职责只剩下“触发一次重绘”。它慢了、漏了几次都没关系,下一次回调算出来的剩余时间依然是准确的。切回前台的瞬间,界面会直接跳到正确的数字。
三、暂停怎么处理
用结束时刻计时后,暂停不能只是停掉定时器,否则恢复时结束时刻已经过期。做法是暂停时把剩余毫秒数存下来,恢复时重新推算结束时刻:
let endAt = 0, remain = 25 * 60 * 1000, running = false;
function start() {
endAt = Date.now() + remain; // 从剩余时间续上
running = true;
}
function pause() {
remain = Math.max(0, endAt - Date.now()); // 记住还剩多少
running = false;
}
四、几个配套细节
- 刷新频率:设成 250 毫秒而不是 1000 毫秒,秒数跳变会更贴近真实时刻,代价可以忽略。
- 取整方式:用 Math.round 而不是 Math.floor,否则刚启动时会立刻从 25:00 跳到 24:59。
- 不要依赖回调判断结束:结束条件写成“剩余时间小于等于 0”,而不是“回调执行了 1500 次”。
- 系统时间被修改:Date.now() 会受用户改系统时间影响。对计时精度要求高的场景,可以改用 performance.now(),它是单调递增的,不受系统时钟调整影响。
五、小结
凡是和真实时间挂钩的功能,倒计时、限时答题、令牌过期,都不应该让定时器来累计时间。定时器只负责“该刷新了”,时间本身永远从时钟现算。记住这一点,后台切换、主线程卡顿都不会再影响结果。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)