V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
CSGO
V2EX  ›  问与答

JS 前端倒计时时间不准确如何解决?

  •  
  •   CSGO · 2021-08-09 09:18:40 +08:00 · 6010 次点击
    这是一个创建于 962 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我在写一个简单的页面,有 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 条附言  ·  2021-08-09 10:07:40 +08:00
    看到了留言,其实原理我明白了,主要是 js 居然有误差,没想到计算机世界里的毫秒时间单位也存在误差,惊叹手表的准确度。以及因为我不是开发者,我做这个就是一个用于 CSGO 的自用和分享朋友用的一个 c4 炸弹计时器。目前来看我的能力只能计算它误差大约多少,然后把 1000ms 修改成类似 980ms 之类的,因为我只需要大约 40 秒一轮回误差不要太大即可。
    54 条回复    2021-08-10 15:48:04 +08:00
    Jackliu
        1
    Jackliu  
       2021-08-09 09:22:43 +08:00
    隔段时间网络校准一次?前提网络良好
    lingo
        2
    lingo  
       2021-08-09 09:23:43 +08:00
    每秒跟本地时间对比一次也好。
    CSGO
        3
    CSGO  
    OP
       2021-08-09 09:27:02 +08:00
    @lingo 好写吗?本地就行,它就是个本地计时器,没必要网络对时。好写的话,我花小钱请朋友写。我不懂代码。
    codehz
        4
    codehz  
       2021-08-09 09:29:59 +08:00 via Android   ❤️ 3
    这思路就错了,应该记录开始时间,然后每次计算流逝的时间,再计算出剩余时间)
    des
        5
    des  
       2021-08-09 09:35:16 +08:00 via iPhone   ❤️ 3
    又一个拿 setInterval 次数用来计时的,你可以试着把你这个 tab 切到后台放一阵,那样就更不准了
    正确应该算时间差
    bnm965321
        6
    bnm965321  
       2021-08-09 09:39:47 +08:00
    因为 JavaScript 一般的运行时是单线程的,可能其它任务正在执行中,不能保证一定在 interval 之后执行这个任务
    wtf12138
        7
    wtf12138  
       2021-08-09 09:44:53 +08:00
    每 100 毫秒 getTime 一下怎么样,就是好像有点浪费
    murmur
        8
    murmur  
       2021-08-09 09:46:41 +08:00
    不要自己去计算时间,用系统的基准时间去对比,你可以半秒刷一次,但是和初始时间+new Date 比
    lingo
        9
    lingo  
       2021-08-09 09:46:41 +08:00
    你是要在开始记录之后分别计时 30 、35 、60 秒嘛。
    那就记录下开始时间的时间戳,分别加上 30 、35 、60 秒的时间,得到三个未来的时间点。
    每秒判断当前的系统时间是否大于那三个时间点。这样只能保证不管过了多长时间,都不会因为 js 的计时导致误差大于 1 秒。。。
    clino
        10
    clino  
       2021-08-09 09:47:48 +08:00
    时间差+1
    xiaojun1994
        11
    xiaojun1994  
       2021-08-09 09:47:54 +08:00
    不应该去++或者--,应该一开始计算截止时间,每次用截止时间来计算
    acthtml
        12
    acthtml  
       2021-08-09 09:49:20 +08:00
    系统计算会消耗时间,所以你每个 interval 其实是 1000ms + 系统消耗。在有些系统的浏览器切换 tab 还会挂起程序,这样的话误差更大。

    可以改成每个 interval 实时计算:剩余时间 = 目标时间 - 当前时间
    CSGO
        13
    CSGO  
    OP
       2021-08-09 09:51:00 +08:00 via Android
    @acthtml 明白了,临时解决方案修改 1000 为 980 之类的。
    CSGO
        14
    CSGO  
    OP
       2021-08-09 09:52:00 +08:00 via Android
    @lingo 好,原理我知道了,谢谢啊。
    zhaol
        15
    zhaol  
       2021-08-09 09:57:38 +08:00
    js 的 setInterval 并不能保证一定是 1000ms 的时候就立即执行,有时候可能因为其他的事件,导致阻塞,然后延迟
    wenzichel
        16
    wenzichel  
       2021-08-09 09:58:01 +08:00
    你不能直接拿上一个数据来计算。而是每次都获取当前的时间与截止时间的差值来显示倒计时。

    这样不会因为其他代码的执行,导致计时不准确,因为每次都会拿当前时间进行校准。

    const lastTime = '2021/12/31 23:59:59';

    setInterval(() => {
    const diff = Date.now() - new Date(lastTime).getTime();
    setNumber( formatTime(diff) ); // formatTime()是将时间戳转为时分秒的方法,请自行实现
    }, 1000);
    Sapp
        17
    Sapp  
       2021-08-09 10:06:09 +08:00
    @CSGO 你这个 980 也依旧不准时,正确的就是记录上次的时间,然后实行的时候获取当前时间,两个相减,再做一些处理(解决切换其他 tab 被暂停的情况),取出一个精确地剩余时间,然后再设置为新的定时器时间,这样应该能准一些,但是如果有其他任务阻塞可能还是不准的,依赖这个准时去做任务本来就不靠谱
    CSGO
        18
    CSGO  
    OP
       2021-08-09 10:08:56 +08:00
    @Sapp 其实原理我明白,主要是 js 居然有误差,以及因为我不是开发者,我做这个就是一个用于 CSGO 的自用和分享朋友用的一个 c4 炸弹计时器。目前来看我的能力只能计算它误差大约多少,然后把 1000ms 修改成类似 980ms 之类的,因为我只需要大约 40 秒一轮回误差不要太大即可。
    ipwx
        19
    ipwx  
       2021-08-09 10:12:12 +08:00
    @CSGO 这不是误差,这是设计。

    你把页面 tab 放后台一段时间,setInterval 就会停下来不会继续运行的。
    des
        20
    des  
       2021-08-09 10:23:17 +08:00 via iPhone
    @CSGO 要说的话,不只 js 有误差,所有语言都可能出现误差
    CSGO
        21
    CSGO  
    OP
       2021-08-09 10:26:38 +08:00 via Android
    @ipwx 后台暂停没关系。我只要记录看的时候计算。大佬有空帮忙写个完整代码吗?请你喝奶茶。
    zenwong
        22
    zenwong  
       2021-08-09 10:29:41 +08:00
    https://github.com/zenboss/BPC.js/blob/master/bpc.js#L27

    const process = () => {
    setTimeout(process, 1e3 - Date.now() % 1e3);
    }
    Jooooooooo
        23
    Jooooooooo  
       2021-08-09 10:31:05 +08:00   ❤️ 7
    做炸弹有误差确实不好.
    CSGO
        24
    CSGO  
    OP
       2021-08-09 10:35:32 +08:00
    mxT52CRuqR6o5
        25
    mxT52CRuqR6o5  
       2021-08-09 10:36:21 +08:00
    用 Date/performance 记录开始时间,然后通过和开始时间比较确定倒计时剩下的时间
    AEDaydreamer
        26
    AEDaydreamer  
       2021-08-09 10:44:18 +08:00
    好奇这个干嘛用的,CS C4 倒计时为什么要用这个。
    CSGO
        27
    CSGO  
    OP
       2021-08-09 10:58:21 +08:00
    otakustay
        28
    otakustay  
       2021-08-09 12:05:26 +08:00
    1. 用 setTimeout 递归代替 setInterval,每次 callback 校准
    2. 用 requestAnimationFrame 超量执行
    phxsuns
        29
    phxsuns  
       2021-08-09 12:12:02 +08:00
    这个倒计时写的不对。
    每次变化都应该取当前时间,然后计算得出你要显示的值。
    lamada
        30
    lamada  
       2021-08-09 12:15:38 +08:00
    写一个看不见的动画,然后监听 transitionend 或者 animationend (滑稽
    dilrvvr
        31
    dilrvvr  
       2021-08-09 12:37:02 +08:00 via iPhone
    应该拿到截止时间,用当前时间去计算剩余时间。不要拿一个数自己去减,代码执行也需要时间的。
    Lemeng
        32
    Lemeng  
       2021-08-09 12:46:28 +08:00
    一点点误差也没多大事吧
    dusu
        33
    dusu  
       2021-08-09 12:46:45 +08:00 via iPhone
    1. 从后台拿到服务器时间 t1
    2. 从前端拿到时间 t2
    3. 计算 t2-t1 时间相差 t3
    4. setinterval 每次刷新 t2,由 t2 - t3 推算到服务器 t1 时间来计算最终倒计时
    CSGO
        34
    CSGO  
    OP
       2021-08-09 13:00:57 +08:00
    @lamada 直接计时都不准确,监听动画来计算时间岂不是也一样。
    learningman
        35
    learningman  
       2021-08-09 13:02:59 +08:00 via Android
    标准的操作是你每隔若干毫秒直接取当前时间计算差。你取延时的话,他肯定会有误差的。
    cyrbuzz
        36
    cyrbuzz  
       2021-08-09 13:34:22 +08:00
    怎么感觉从理论上不会有误差呢,vant 的倒计时也是用的 raf 和 setTimeout 做的。就楼主这点代码还能阻塞到有误差?来个大佬解释一下。

    从解决的角度来看,vant 内的秒级并不是 1s 执行一次,而是 1 秒执行 60 次(raf 或者 setTimeout 的模拟,或者用 requestIdleCallback 来找空闲时间的回调),通过比对是否秒数一致来实现的。
    ODD10
        37
    ODD10  
       2021-08-09 13:40:06 +08:00 via iPhone
    @lamada #30
    你知道动画的实现?
    话说动画不会卡?
    Sasasu
        38
    Sasasu  
       2021-08-09 14:24:41 +08:00
    用 requestAnimationFrame
    YadongZhang
        39
    YadongZhang  
       2021-08-09 15:54:48 +08:00
    Biwood
        40
    Biwood  
       2021-08-09 16:33:41 +08:00
    @cyrbuzz
    其实问题的本质不在于用 raf 还是 setTimeout,而在于你是用计时器的默认间隔时间来计时,还是用系统自带的时间戳来计时。

    比如 setTimeout(fn, 0)这种写法,看起来应该是计时器设置后 0 秒马上触发回调函数对吧,而实际上至少都有个十几毫秒的时间差,如果对 JS 引擎的事件循环有足够了解就知道其中原因了,用计时器的执行间隔时间来计时肯定是不准确的。想象一下如果代码里面出现了 while(true){...} 这一类的阻塞代码,或者切换到别的 tab 导致页面暂停了计时器任务,还怎么保证计时的准确性?

    所以更准确的方式应该是频繁利用 Date 接口读取系统的时间戳,然后通过计算时间戳之间的差值来更新计时,当然这要求刷新频率应该小于一秒钟,像你说的一秒钟执行 60 次是个不错的方案。
    matepi
        41
    matepi  
       2021-08-09 16:49:02 +08:00
    setTimeout,里面不是做自增自减

    而是应该做目标差

    即便不是 js,是多任务的 interval 从来就是不准的,早年就有由于晶振原因,最小时间片 56 还是 58ms 之说。以延时 1000ms,也只是获得了最接近的时间。

    因此当年写反复延时,都得知道要靠做差法,而不是每次延时。

    作差法也有问题,就是用来做显示的事件触发化,有低概率在同一秒内,事件如显示的 update 这次不会触发。
    netwjx
        42
    netwjx  
       2021-08-09 17:14:22 +08:00
    UI 编程永远都会有类似的毫秒级误差问题, 不仅仅是浏览器端, android, ios 都一样

    因为不能允许多线程共同操作 UI 资源, 会线程冲突


    setInterval 每次的误差应该是毫秒级, 60 次就能累积到 1s 还是有点高了
    更进一步的办法类似 41 楼说 算目标时间差

    这是动画 /游戏开发的技巧, 确保在不同的设备上实际时间一致
    cyrbuzz
        43
    cyrbuzz  
       2021-08-09 17:53:53 +08:00
    @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 。
    Quarter
        44
    Quarter  
       2021-08-09 17:59:54 +08:00 via iPhone
    倒计时可以不用手动—,可以记录开始的时间戳,然后定时执行此时的时间戳和开始的时间戳的差值对比倒计时显示结果,这样应该可以避免你说的时间误差问题🦖
    Quarter
        45
    Quarter  
       2021-08-09 18:03:22 +08:00 via iPhone
    另外,也可以不用 setInterval,用 requestAnimationFrame/cancel AnimationFrame 的 API,当然,计算次数会变多,但是更精准一些
    Biwood
        46
    Biwood  
       2021-08-09 18:16:25 +08:00
    @cyrbuzz
    眼睛看肯定是看不出来的,具体延迟多少不仅取决于代码逻辑,还跟计算机本身的运行状态等因素也有关系,这是很难确定的
    des
        47
    des  
       2021-08-09 18:48:17 +08:00 via iPhone
    @cyrbuzz 你可以算一下 60hz 下,一帧平均能分到多长时间,看看这个时间有没有很眼熟?
    lysS
        48
    lysS  
       2021-08-09 20:11:19 +08:00
    在后台循环打印 🐸

    🐶
    CSGO
        49
    CSGO  
    OP
       2021-08-09 20:45:49 +08:00 via Android
    @cyrbuzz 当电脑比较卡的时候,就是 CPU 占用率高的时候延时越高。
    crclz
        50
    crclz  
       2021-08-09 22:56:17 +08:00   ❤️ 1
    我记得上学的时候做过一个定时器。老师想要全班考试的时候能看到倒计时,有紧迫感。
    用的是 Thread.Sleep,结果第一版出来,考个理综,我们班比其他班多 5 分钟时间,哈哈哈。
    kyuuseiryuu
        51
    kyuuseiryuu  
       2021-08-09 23:43:19 +08:00 via iPhone
    setTimeout 和 setInterval 第二个参数的意思是回调函数最快被调用的时间限制。
    cyrbuzz
        52
    cyrbuzz  
       2021-08-10 10:34:52 +08:00   ❤️ 1
    @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,多了一次....= =。
    Kinnice
        53
    Kinnice  
       2021-08-10 15:42:15 +08:00   ❤️ 1
    @cyrbuzz 破案
    CSGO
        54
    CSGO  
    OP
       2021-08-10 15:48:04 +08:00 via Android   ❤️ 1
    @cyrbuzz 啊,这这样啊。。。我有放在网络上: https://csgo.link/web/c4time
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2720 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 390ms · UTC 15:40 · PVG 23:40 · LAX 08:40 · JFK 11:40
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.