
我在写一个简单的页面,有 3 个倒计时,在别人的帮助下,成功实现了功能,但是发现 js 代码在不同浏览器下,倒计时都有误差,大约每 60 秒就会慢 1 秒钟左右,有没什么办法修复它?
function setNumber(elem, num) { elem.innerText = num.toString().padStart(2, '0'); } function countdown(elem, init) { setNumber(elem, init); return setInterval(() => { let num = Number(elem.innerText); let next = num - 1; if (next < 0) next = init; setNumber(elem, next); }, 1000); } function init() { var timeList = [ { 'name': 'explode-id', 'node': 'wifi-explode', 'time': 60 }, { 'name': 'nokit-id', 'node': 'wifi-nokit', 'time': 30 }, { 'name': 'ownkit-id', 'node': 'wifi-ownkit', 'time': 35 } ]; timeList.forEach(function (item) { clearInterval(parseInt(document.getElementById(item['name']).innerHTML)); document.getElementById(item['name']).innerHTML = countdown(document.getElementsByClassName(item['node'])[0], item['time']); }); } let wifiClick = document.getElementsByClassName('wifi-click')[0]; init(); wifiClick.addEventListener('click', () => { init(); }) 1 Jackliu Aug 9, 2021 隔段时间网络校准一次?前提网络良好 |
2 lingo Aug 9, 2021 每秒跟本地时间对比一次也好。 |
4 codehz Aug 9, 2021 via Android 这思路就错了,应该记录开始时间,然后每次计算流逝的时间,再计算出剩余时间) |
5 des Aug 9, 2021 via iPhone 又一个拿 setInterval 次数用来计时的,你可以试着把你这个 tab 切到后台放一阵,那样就更不准了 正确应该算时间差 |
6 bnm965321 Aug 9, 2021 因为 Javascript 一般的运行时是单线程的,可能其它任务正在执行中,不能保证一定在 interval 之后执行这个任务 |
7 milkzizi Aug 9, 2021 每 100 毫秒 getTime 一下怎么样,就是好像有点浪费 |
8 murmur Aug 9, 2021 不要自己去计算时间,用系统的基准时间去对比,你可以半秒刷一次,但是和初始时间+new Date 比 |
9 lingo Aug 9, 2021 你是要在开始记录之后分别计时 30 、35 、60 秒嘛。 那就记录下开始时间的时间戳,分别加上 30 、35 、60 秒的时间,得到三个未来的时间点。 每秒判断当前的系统时间是否大于那三个时间点。这样只能保证不管过了多长时间,都不会因为 js 的计时导致误差大于 1 秒。。。 |
10 clino Aug 9, 2021 时间差+1 |
11 xiaojun1994 Aug 9, 2021 不应该去++或者--,应该一开始计算截止时间,每次用截止时间来计算 |
12 acthtml Aug 9, 2021 系统计算会消耗时间,所以你每个 interval 其实是 1000ms + 系统消耗。在有些系统的浏览器切换 tab 还会挂起程序,这样的话误差更大。 可以改成每个 interval 实时计算:剩余时间 = 目标时间 - 当前时间 |
15 zhaol Aug 9, 2021 js 的 setInterval 并不能保证一定是 1000ms 的时候就立即执行,有时候可能因为其他的事件,导致阻塞,然后延迟 |
16 wenzichel Aug 9, 2021 你不能直接拿上一个数据来计算。而是每次都获取当前的时间与截止时间的差值来显示倒计时。 这样不会因为其他代码的执行,导致计时不准确,因为每次都会拿当前时间进行校准。 const lastTime = '2021/12/31 23:59:59'; setInterval(() => { const diff = Date.now() - new Date(lastTime).getTime(); setNumber( formatTime(diff) ); // formatTime()是将时间戳转为时分秒的方法,请自行实现 }, 1000); |
17 Sapp Aug 9, 2021 @CSGO 你这个 980 也依旧不准时,正确的就是记录上次的时间,然后实行的时候获取当前时间,两个相减,再做一些处理(解决切换其他 tab 被暂停的情况),取出一个精确地剩余时间,然后再设置为新的定时器时间,这样应该能准一些,但是如果有其他任务阻塞可能还是不准的,依赖这个准时去做任务本来就不靠谱 |
18 CSGO OP @Sapp 其实原理我明白,主要是 js 居然有误差,以及因为我不是开发者,我做这个就是一个用于 CSGO 的自用和分享朋友用的一个 c4 炸弹计时器。目前来看我的能力只能计算它误差大约多少,然后把 1000ms 修改成类似 980ms 之类的,因为我只需要大约 40 秒一轮回误差不要太大即可。 |
22 zenwong Aug 9, 2021 https://github.com/zenboss/BPC.js/blob/master/bpc.js#L27 const process = () => { setTimeout(process, 1e3 - Date.now() % 1e3); } |
23 Jooooooooo Aug 9, 2021 做炸弹有误差确实不好. |
24 CSGO OP @Jooooooooo ... |
25 mxT52CRuqR6o5 Aug 9, 2021 用 Date/performance 记录开始时间,然后通过和开始时间比较确定倒计时剩下的时间 |
26 AEDaydreamer Aug 9, 2021 好奇这个干嘛用的,CS C4 倒计时为什么要用这个。 |
27 CSGO OP |
28 otakustay Aug 9, 2021 1. 用 setTimeout 递归代替 setInterval,每次 callback 校准 2. 用 requestAnimationFrame 超量执行 |
29 phxsuns Aug 9, 2021 这个倒计时写的不对。 每次变化都应该取当前时间,然后计算得出你要显示的值。 |
30 lamada Aug 9, 2021 写一个看不见的动画,然后监听 transitionend 或者 animationend (滑稽 |
31 dilrvvr Aug 9, 2021 via iPhone 应该拿到截止时间,用当前时间去计算剩余时间。不要拿一个数自己去减,代码执行也需要时间的。 |
32 Lemeng Aug 9, 2021 一点点误差也没多大事吧 |
33 dusu Aug 9, 2021 via iPhone 1. 从后台拿到服务器时间 t1 2. 从前端拿到时间 t2 3. 计算 t2-t1 时间相差 t3 4. setinterval 每次刷新 t2,由 t2 - t3 推算到服务器 t1 时间来计算最终倒计时 |
35 learningman Aug 9, 2021 via Android 标准的操作是你每隔若干毫秒直接取当前时间计算差。你取延时的话,他肯定会有误差的。 |
36 cyrbuzz Aug 9, 2021 怎么感觉从理论上不会有误差呢,vant 的倒计时也是用的 raf 和 setTimeout 做的。就楼主这点代码还能阻塞到有误差?来个大佬解释一下。 从解决的角度来看,vant 内的秒级并不是 1s 执行一次,而是 1 秒执行 60 次(raf 或者 setTimeout 的模拟,或者用 requestIdleCallback 来找空闲时间的回调),通过比对是否秒数一致来实现的。 |
37 uTOmOuk3L6sb4MSI Aug 9, 2021 via iPhone |
38 Sasasu Aug 9, 2021 用 requestAnimationFrame |
39 zhyd1997 Aug 9, 2021 |
40 Biwood Aug 9, 2021 @cyrbuzz 其实问题的本质不在于用 raf 还是 setTimeout,而在于你是用计时器的默认间隔时间来计时,还是用系统自带的时间戳来计时。 比如 setTimeout(fn, 0)这种写法,看起来应该是计时器设置后 0 秒马上触发回调函数对吧,而实际上至少都有个十几毫秒的时间差,如果对 JS 引擎的事件循环有足够了解就知道其中原因了,用计时器的执行间隔时间来计时肯定是不准确的。想象一下如果代码里面出现了 while(true){...} 这一类的阻塞代码,或者切换到别的 tab 导致页面暂停了计时器任务,还怎么保证计时的准确性? 所以更准确的方式应该是频繁利用 Date 接口读取系统的时间戳,然后通过计算时间戳之间的差值来更新计时,当然这要求刷新频率应该小于一秒钟,像你说的一秒钟执行 60 次是个不错的方案。 |
41 matepi Aug 9, 2021 setTimeout,里面不是做自增自减 而是应该做目标差 即便不是 js,是多任务的 interval 从来就是不准的,早年就有由于晶振原因,最小时间片 56 还是 58ms 之说。以延时 1000ms,也只是获得了最接近的时间。 因此当年写反复延时,都得知道要靠做差法,而不是每次延时。 作差法也有问题,就是用来做显示的事件触发化,有低概率在同一秒内,事件如显示的 update 这次不会触发。 |
42 netwjx Aug 9, 2021 UI 编程永远都会有类似的毫秒级误差问题, 不仅仅是浏览器端, android, ios 都一样 因为不能允许多线程共同操作 UI 资源, 会线程冲突 setInterval 每次的误差应该是毫秒级, 60 次就能累积到 1s 还是有点高了 更进一步的办法类似 41 楼说 算目标时间差 这是动画 /游戏开发的技巧, 确保在不同的设备上实际时间一致 |
43 cyrbuzz Aug 9, 2021 @Biwood 还是有点疑问,setTimeout 设置 0 会有最小 4ms 的延迟是清清楚楚在文档里写的,非活跃的标签在各个浏览器下对定时器也有节流在文档里也可以清楚看到,raf 非活跃不会执行。 我的疑问是仅从楼主代码来看,1000ms 这个为什么会有延迟,楼主的代码本身没有会阻塞很长时间的代码,楼主说 60 秒会慢 1 秒,也就是每次大概都会有 16.66ms 的延迟,为什么会有如此大的延迟?楼主定时器的内的代码: ``` let num = Number(elem.innerText); let next = num - 1; if (next < 0) next = init; elem.innerText = num.toString().padStart(2, '0'); ``` 他怎么看都花不了 16ms 。 |
44 Quarter Aug 9, 2021 via iPhone 倒计时可以不用手动,可以记录开始的时间戳,然后定时执行此时的时间戳和开始的时间戳的差值对比倒计时显示结果,这样应该可以避免你说的时间误差问题 |
45 Quarter Aug 9, 2021 via iPhone 另外,也可以不用 setInterval,用 requestAnimationFrame/cancel AnimationFrame 的 API,当然,计算次数会变多,但是更精准一些 |
48 lysS Aug 9, 2021 在后台循环打印 |
50 crclz Aug 9, 2021 我记得上学的时候做过一个定时器。老师想要全班考试的时候能看到倒计时,有紧迫感。 用的是 Thread.Sleep,结果第一版出来,考个理综,我们班比其他班多 5 分钟时间,哈哈哈。 |
51 Chemist Aug 9, 2021 via iPhone setTimeout 和 setInterval 第二个参数的意思是回调函数最快被调用的时间限制。 |
52 cyrbuzz Aug 10, 2021 @Biwood @CSGO @des 谢谢各位大佬的回复,感觉大佬们的回复没有解决我的疑问= =...可能是我理解力不够,我承认 CPU 处理不过来内容,掉帧情况等等都会有误差产生,否则也不会衍生出 raf, ric 这些按帧回调,空闲回调的 API 了。 但我还是不相信楼主这种代码都会产生 1s 的误差,所以做了一下测试,因为楼主没有贴 HTML 部分代码稍微还原了一下: ``` <body> <div class="wifi-click"> </div> <div id="explode-id"> </div> <div id="nokit-id"> </div> <div id="ownkit-id"> </div> <div class="wifi-explode"> </div> <div class="wifi-nokit"> </div> <div class="wifi-ownkit"> </div> </body> <script> function setNumber(elem, num) { elem.innerText = `${num.toString().padStart(2, '0')}<br>${new Date()}`; } function countdown(elem, init) { setNumber(elem, init); return setInterval(() => { let num = Number(elem.innerText.split('<br>')[0]); let next = num - 1; if (next < 0) next = init; setNumber(elem, next); }, 1000); } function init() { var timeList = [ { 'name': 'explode-id', 'node': 'wifi-explode', 'time': 60 }, { 'name': 'nokit-id', 'node': 'wifi-nokit', 'time': 30 }, { 'name': 'ownkit-id', 'node': 'wifi-ownkit', 'time': 35 } ]; timeList.forEach(function (item) { clearInterval(parseInt(document.getElementById(item['name']).innerHTML)); document.getElementById(item['name']).innerHTML = countdown(document.getElementsByClassName(item['node'])[0], item['time']); }); } let wifiClick = document.getElementsByClassName('wifi-click')[0]; init(); wifiClick.addEventListener('click', () => { init(); }) wifiClick.innerHTML = new Date() </script> ``` 在楼主代码基础上加了 new Date()方便观察,一开始这段代码确实会每分钟都比 new Date 慢一秒,此时我的电脑都处于 CPU 占用率 0~1%之间,记录 Performance 也没有发现异常。 直到注意到楼主的定时器代码里: ``` let next = num - 1; if (next < 0) next = init; ``` 当 next 小于 0 时才重置,这样就导致本应该 1 分钟 0~59,1~60 的循环变成了 0~60,多了一次....= =。 |
54 CSGO OP @cyrbuzz 啊,这这样啊。。。我有放在网络上: https://csgo.link/web/c4time |