为什么前端倒计时活动不宜直接用setTimeoutsetInterval?
摘要:在商城项目中,「倒计时活动」几乎是绕不开的需求: 秒杀、限时优惠、拼团、支付剩余时间…… 我相信很多都跟我一样一开始写出类似这样的代码: setInterval(() => { remainTime-- }, 1000
在商城项目中,「倒计时活动」几乎是绕不开的需求:
秒杀、限时优惠、拼团、支付剩余时间……
我相信很多都跟我一样一开始写出类似这样的代码:
setInterval(() => {
remainTime--
}, 1000)
功能能跑,但线上问题也会跟着跑出来。
一、为什么 setTimeout / setInterval 不适合活动倒计时?
它们天生就不准
很多人对定时器有一个误解:
setInterval(fn, 1000) ≠ 每 1000ms 准时执行
原因只有一个:
JavaScript 是单线程的。
到这你先回想一下事件循环机制,有哪些?然后再往下看;如果实在想不起来就先上网搜下,或者看看我的单线程原理。
事件循环机制
主线程被渲染卡住
执行大量 JS
GC、布局、重绘
都会导致定时器回调被延后执行。
倒计时的表现就是:
跳秒
变慢
和真实时间对不上
浏览器会「故意」降级定时器
这是活动倒计时最容易翻车的一点。
当页面进入以下状态:
后台 Tab
页面最小化
手机锁屏
浏览器会主动做这些事:
延长定时器触发间隔
甚至直接暂停执行
结果就是:
用户切个 Tab 回来,
倒计时还显示 10 秒,
实际活动已经结束。
对活动类业务,这是不能接受的。
setTimeout 递归,本质问题没变
可能有的会写成这样:
function tick() {
setTimeout(() => {
remainTime--
tick()
}, 1000)
}
看起来比 setInterval 稳一点,但实际上:
依然受线程影响
依然受浏览器限流
依然不可靠
只是“写法高级了”,问题没解决。
二、倒计时的核心思路必须反过来
错误思路(很多第一版代码)
“我现在有 60 秒,每秒减 1”
正确思路(真实业务)
“活动有一个确定的结束时间点,我只计算当前时间与结束时间的差值”
正确的倒计时模型
后端返回活动结束时间戳(endTime)
前端永远不存“剩余秒数”
每次渲染时:
const remain = endTime - Date.now()
前提是: Date.now() 是可信的
这样做的好处是:
页面卡顿不影响
切 Tab 不影响
页面刷新不影响
时间一定是真实世界的时间
三、requestAnimationFrame 在倒计时里的正确用法
那么有的就得来犟一下:
那是不是可以用 requestAnimationFrame?
nonono,是这样的
requestAnimationFrame 适合“展示型倒计时”,不适合直接当计时器。
为什么 rAF 比 setInterval 好一点?
跟随浏览器刷新节奏(通常 60fps)
页面不可见时自动暂停(省性能)
不会出现多个定时器竞争
但它的问题也很明显:
后台直接停
不保证时间间隔
本质还是“帧驱动”,不是“时间驱动”
正确用法:rAF + 时间戳差值
function startCountdown(endTime, update) {
function loop() {
const remain = endTime - Date.now()
if (remain <= 0) {
update(0)
return
}
update(remain)
requestAnimationFrame(loop)
}
loop()
}
rAF 只负责触发更新
时间完全由 Date.now() 决定
不用 rAF 去「数秒」
这种方式非常适合:
大屏倒计时
动画数字变化
强 UI 表现的倒计时
四、真实业务里的性能坑
多个倒计时 = 多个定时器
列表页如果有 20 个活动:
20 个 setInterval
页面性能直线下降
组件卸载忘记清理
内存泄漏
幽灵定时器
难以排查的线上问题
前后端时间不同步
前端显示没结束
后端接口已判定结束
用户点击直接报错
五、重点来了:第三方方案怎么选?
dayjs / date-fns(强烈推荐)
它们不是倒计时库,但非常适合做倒计时。
