乐趣区

关于javascript:如何在-JavaScript-中获得准确的倒计时

家喻户晓,setTimeout 意味着在最小阈值(m 单位)之后运行脚本,setInterval 意味着以最小阈值周期间断执行指定的脚本。请留神,我在这里应用术语“最小阈值”,因为它并不总是精确的。

为什么 setTimeout 和 setInterval 不精确?

要答复这个问题,你首先须要理解 JavaScript 宿主环境(browser 或 Node.js)中有一种称为事件循环的机制。当初简略介绍一下:

首先,为什么要有一个事件循环?

因为对于浏览器来说,它须要接管用户的交互来实现一些 UI 的扭转,而 JS 脚本是在浏览器中单线程运行的,所以不能间接晓得用户的操作。所以这里减少了一个事件循环机制,浏览器外部过程告诉 JS 让 JS 执行指定的脚本。
对于 setTimeout 和 setIntervalAPI,它们是非凡的,因为它们都能够生成宏工作。那么什么是宏工作呢?

宏工作能够了解为须要 JS 执行的工作。例如,用户交互点击会生成一个宏工作,浏览器会将点击的宏工作增加到工作队列中。当 JS 执行引擎闲暇时,会取出队列头部的工作执行,这样点击绑定的回调函数就能够执行了。

也就是说,setTimeout 并且 setInterval 不会立刻执行。当咱们在代码中调用它们时,首先会将它们退出到工作队列中进行排队,而后 JS 执行引擎会在闲暇时从队列头部取出工作执行。

如果是定时工作,会查看是否超时。如果已过期,将被取出并执行。如果没有过期,就会执行上面的宏工作。
执行实现后会开始新一轮的循环,持续查看工作队列的头部。

这也解释了为什么它们不精确,因为当它们退出工作队列时,可能曾经有工作排在它们后面,而它们必须排在这些工作前面。

因而,当后面的工作实现,轮到他们执行时,实时距离可能曾经超过了咱们传递的值,这就是最小阈值的由来。实时距离只能大于或等于咱们传递的值。

事件循环机制能够查看其余材料,此处疏忽了微工作、宏工作的区别,只为了阐明为什么会导致计时不精确。

let count = 0;
const startTime = Date.now();

setInterval(() => {
  count += 1000;
  console.log('deviation', Date.now() - (startTime + count));
}, 1000);

// deviation: 2
// deviation: 5
// deviation: 4
// deviation: 3

从下面的代码中,咱们能够看出 setInterval 总是不精确的。如果在代码中增加耗时工作,差别会越来越大(setTimeout 是一样的)。

如何失去一个绝对精确的 setInterval?

1. 应用 while

简略粗犷,咱们能够间接用 while 语句阻塞主线程,一直计算以后工夫和下一次的时间差。一旦大于等于 0,立刻执行。
代码如下:

function sleepInterval(cb, time) {let count = 0, startTime = Date.now()

    while (count <= time) {let nowTime = Date.now() - startTime
        let nextTime = count + 1000
        if (nowTime >= nextTime) {
            count = nextTime
            cb && cb();}
    }
}

function foo() {console.log('foo')
}

sleepInterval(loga, 3000)

此函数能够用作 js 里的 sleep 函数,多了定时执行的性能

这种办法能够管制差值稳固在 0,然而这个办法阻塞了 JS 执行线程,使得 JS 执行线程无奈进行并从队列中取出工作。这会导致页面解冻,无奈响应任何操作。这是破坏性的,因而不可取。

2. 应用 requestAnimationFrame

浏览器提供了 requestAnimationFrame API,它通知浏览器你要执行一个动画,并要求浏览器在下次重绘前调用指定的回调函数更新动画。该回调函数将在浏览器下一次重绘之前执行。每秒的执行次数会依据屏幕的刷新率来决定。60Hz 的刷新率意味着每秒会有 60 次,也就是 16.7ms 左右。

function intervalTimer(time) {
    let counter = 1;
    const startTime = Date.now();
    function main() {const nowTime = Date.now();
        const nextTime = startTime + counter * time;
        if (nowTime - nextTime >= 0) {console.log('deviation', nowTime - nextTime);
            counter += 1;
        }
        window.requestAnimationFrame(main);
    }
    main();}
intervalTimer(1000);
// deviation 5
// deviation 7
// deviation 9
// deviation 12

咱们能够发现,因为 16.7ms 的距离执行,很容易造成工夫不精确。

3. 应用 setTimeout + 零碎工夫偏移

该计划的原理是利用以后零碎的精确工夫在每次之后进行弥补和校对,setTimeout 以保障前面的计时工夫是弥补后的工夫,从而减小时间差。

function intervalTimer(callback, interval = 500) {
    let counter = 1;
    let timeoutId;
    const startTime = Date.now();

    function main() {const nowTime = Date.now();
        const nextTime = startTime + counter * interval;
        timeoutId = setTimeout(main, interval - (nowTime - nextTime));

        console.log('deviation', nowTime - nextTime);

        counter += 1;
        callback();}

    timeoutId = setTimeout(main, interval);

    return () => {clearTimeout(timeoutId);
    };
}

let value = 10;

const cancelTimer = intervalTimer(() => {if (value > 0) {value -= 1;} else {cancelTimer();
    }
}, 1000);
// deviation 3
// deviation 1
// deviation 0
// deviation 2

能够看到,这个计划的时间差比拟小,而且可预感的差值不会随着工夫的推移而逐步增大,总是稳固在能够承受的范畴内。

综合来看,咱们在不阻塞主线程的状况下实现稳固且绝对精确的 setInterval,第三种形式这可能是取得精确倒计时的最佳解决方案。

号外!!!每天一点小精进,举荐你关注一下前端之路小程序,外面有你想看的热门文章、定时推送的前端周刊、面试必刷的八股文汇合,更次要的是咱们有一帮气味相投的酷爱前端开发的小可爱们~

退出移动版