之前一直记得setInterval有一些坑,但是具体是哪些坑一直不太清楚。最近我进行了一番调查,决定记录下来,以免自己忘记,并希望更多人了解这些坑。

首先,setInterval会无视代码的错误。即使发生错误,它仍然会继续循环执行,不会停止。这可能导致你的代码存在一些问题(比如代码可能会有概率性错误,而你使用setInterval来循环调用它,因为setInterval不会因为报错而停止,所以这个问题可能被隐藏),但很难发现。

例如:

let count = 1;
setInterval(function () {
    count++;
    console.log(count);
    if (count % 3 === 0) throw new Error('setInterval报错');
}, 1000);

另外,setInterval会在任何情况下定时执行。在某些场景下,我们并不希望如此。

例如,我们想要实现一个功能,每隔一段时间向服务器发送请求以查看是否有新数据。如果用户的网络状态很差,客户端收到请求响应的时间大于interval循环的时间,那么setInterval会无视这个情况,导致用户的客户端充满了ajax请求。正确的做法是改用setTimeout,在用户的请求得到响应或超时后,再使用setTimeout递归发送下一个请求。这样就不会有setInterval的坑了。

此外,setInterval不能确保每次调用都能被执行。我们来看一个例子:

const startDate = new Date();
let endDate;

// 第一个调用会被略过
setInterval(() => {
    console.log('start');
    console.log(startDate.getTime());
    console.log(endDate.getTime());
    console.log('end');
}, 1000);

while (startDate.getTime() + 2 * 1000 > (new Date()).getTime()) {}

endDate = new Date();

可以看到,第一次执行setInterval函数时,输出的startDate和endDate相差超过2秒。而我们的setInterval设置为每隔1秒执行一次。因此,可以看出第一次的setInterval函数调用被略过了。

这说明如果你的代码执行时间较长,就会导致setInterval中的某些函数调用被略过。所以如果你的程序依赖于setInterval的精确执行,就要小心这一点。

当然,setTimeout也存在这个问题。浏览器的定时器都不是精确执行的。即使调用setTimeout(fn, 0),也不能确保立即执行。

针对这些问题,我们可以使用setTimeout,并在其中递归调用自身来解决。

例如,前面提到的两个坑可以这样改写:

function fn() {
    setTimeout(() => {
        // 程序主逻辑代码
        // 循环递归调用
        fn();
    }, 1000);
}

fn();

然而,使用setTimeout后可能会遇到一个新问题,即计时器的下次触发时间是相对于当前触发时间计算的。对于第二个坑来说,这样的处理是合理的,但有时我们希望计时器能够“匀速”触发,即希望触发时间尽可能在计时器注册时间加上周期乘以延迟附近。这时,我们可以通过预期的下次触发时间减去当前时间来得到一个精确的延迟时间。

我写了一个简单的函数来实现这一点:首次调用该函数时,记录当前的计时器注册时间和一个用于统计调用次数的变量。之后每次调用newFn时,都使用预期的下次触发时间减去当前时间得到一个精确的延迟时间。这样至少能保证在某些情况下,计时器能稍微精确地执行。

function accurateTimers(fn, expectDelayTime) {
    let initialized = false;
    let registeredDate = new Date(); // 计时器注册时间
    let count = 0; // 计时器调用次数

    function newFn() {
        let delayTime;
        count++;
        if (!initialized) {
            initialized = true;
            delayTime = expectDelayTime;
        } else {
            delayTime = expectDelayTime * count + registeredDate.getTime() - new Date().getTime();
        }
        console.log(delayTime);
        setTimeout(() => {
            fn();
            newFn();
        }, delayTime);
    }

    newFn();
}

accurateTimers(function () {
    let startDate = new Date();
    // 延迟500ms
    while (startDate.getTime() + 500 > (new Date()).getTime()) {}
}, 1000);

综上所述,这就是本文的内容。希望通过这篇文章,大家能了解到setInterval的坑,并在实际编程中少走弯路。