一、为什么setInterval不能实现
原因有两:1、js是单线程,基于事件循环执行其他任务(这里建议读者可以多去了解一下浏览器线程与事件循环相关知识)
2、setinterval是每隔delay时间,把逻辑放到任务队列中,而不是执行栈中,且如果setinterval中的回调函数执行较长时间,如有for循环等逻辑,那么随着每一次执行误差会越来越大,(会因执行延迟导致误差累积)
二、如何解决
1、webworker:适合长时间定时
原理:Worker独立于主线程运行,不受DOM渲染/事件处理等任务阻塞
思路:主线程向worker线程发送倒计时开始时间戳和总时长(ms)
worker线程预先计算结束时间戳,通过结束时间戳 - 当前时间戳
计算剩余时间,避免传统递减法的累积误差,通过postMessage向主线程发送(基于绝对时间计算剩余时间,而不是简单递减)
主线程监听并显示UI
self.onmessage = function(e) {// 1. 接收主线程传递的初始参数const { startTime, duration } = e.data; // 开始时间戳和总时长(ms)const endTime = startTime + duration; // 预先计算结束时间戳function update() {// 2. 计算精确剩余时间const remaining = Math.max(0, endTime - Date.now());
// 这里也可以用performance.now() ws级别// 3. 向主线程发送实时数据self.postMessage({remaining: remaining, // 精确到毫秒的剩余时间seconds: Math.ceil(remaining / 1000) // 向上取整的秒数(UI显示用)});if (remaining > 0) {// 4. 动态计算下次触发时间(误差补偿核心)const nextTick = remaining % 1000 || 1000;setTimeout(update, nextTick);}};update(); // 首次启动
};
有个缺点:就是主线程接收到应该渲染的事件后,也是需要去等待当前执行栈清空(比如正在运行的JS代码)和到达浏览器渲染周期才去渲染,实际延迟通常小于1帧(<16ms)这个方法还是有些不足,但是比传统的setinterval会好很多
2、requestAnimation:适合短时间定时
思路:与浏览器刷新率同步+performance.now()
微秒级别+requestAnimation去控制UI更新
这里有个注意点requestAnimation是控制UI渲染的,也就是会让倒计时ui16.6ms就会更新一次,避免了上一个方法中因为事件循环导致ui渲染的去查,精确计算剩余时间靠 performance.now()实现
function update() {const remaining = endTime - performance.now(); // 计算剩余时间if (remaining <= 0) {show("时间到!");return;}// 更新UI(但不需要每秒更新60次!)show(Math.ceil(remaining / 1000) + "秒"); requestAnimationFrame(update); // 继续循环
}requestAnimationFrame(update); // 启动
3、最好的是requestanimation与webworker+performance.now()结合 requestanimation解决ui渲染问题,
webworker+performance.now()解决精确计时问题
4、服务器同步时间:适合抢购、秒杀等场景
-
同步阶段:
-
获取服务器时间与本地时间的差值
timeDiff
-
-
倒计时阶段:
-
始终通过
Date.now() + timeDiff
获取真实服务器时间 -
基于服务器时间计算剩余时长
-