1

问题

经常使用Javascript的同学一定对setInterval非常熟悉,当使用setInterval(callback, timer)时,每经过timer毫秒时间,系统都将调用一次callback。请问全局如果没有提供setInterval函数,该如何自己实现这一功能?

方案一:循环或递归(错误解法)

最简单的思路便是通过简单的循环或者递归,每次检查时间戳是否已经超过上次触发给定函数的时间加上间隔时间,如果已经超过便再次触发函数,并重置计时器至当前时间。

const setInterval1 = (func, interval) => {
    let startTime = Date.now();
    const config = { shouldStop: false };
    while (!config.shouldStop) {
        if (Date.now() - startTime >= interval) {
            func();
            startTime = Date.now();
        }
    }
    return config;
}

const myClearInterval = config => { config.shouldStop = true; }

然而这样的解法有一个致命问题,我们将setInterval1变成一个阻塞函数,主线程会卡死在这个无限循环或者递归中,导致之后的代码或者事件无法执行。想了解详细原因的请戳: 并发模型与事件循环JavaScript:彻底理解同步、异步和事件循环(Event Loop)

方案二:使用setTimeout

setTimeout的好处在于,它是在消息队列里面添加一个待执行的消息,所以并不会堵塞主线程。更方便的在于,由于setTimeout自带定时器功能,我们甚至不用自己去维护一个时间戳。我们可以通过不断递归调用setTimeout来实现setInterval的效果

const setInterval2 = (func, interval) => {
    const config = { shouldStop: false }
    const loop = () => {
        if (!config.shouldStop) {
            func();
            setTimeout(loop, interval);
        }
    }
    setTimeout(loop, interval);
    return config;
}

const myClearInterval = config => { config.shouldStop = true; }

方案三:使用requestAnimationFrame

然而使用setTimeout有违这道题的初衷,因为setTimeout在本质上和setInterval是类似的,多少有些作弊的嫌疑。那有没有别的非阻塞方案呢?在浏览器环境中,我们有requestAnimationFrame(),而在nodejs环境中,我们有setImmediate()。以requestAnimationFrame为例,这将保证我们的代码只会在每一帧render之前被递归一次,从而避免了阻塞其他代码。

const setInterval3 = (func, interval) => {
    let startTime = Date.now();
    const config = { shouldStop: false }
    const check = () => {
        if (!config.shouldStop) {
            if (Date.now() - startTime > interval) {
                func();
                startTime = Date.now();
            }
            if(typeof window === 'undefined') {
                setImmediate(check);
            } else {
                window.requestAnimationFrame(check)
            }
        }
    }
    check();
    return config;
}

const myClearInterval = config => { config.shouldStop = true; }

方案四:使用Web Worker

requestAnimationFrame能确保我们在每帧显示前被调用一次,从而检计时器是否到期,但是如果被执行的函数计算量极大,导致帧内无法完成时,该如何保证给定函数能按时执行呢?显然,此时只依靠主线程来确保计时程序和给定程序都能准确执行,有点困难,但是如果将计时程序放入另一线程中,而主程序只负责监听定时器事件和执行给定程序,是不是会好一些呢?所以我们这里利用浏览器提供的Web Worker API来实现多线程。请注意这里由于没有调用另一个脚本,我们通过blob和object url的方式将我们的定时器程序check传入Web Worker中。

const setInterval4 = (func, interval) => {
    if (typeof window !== 'undefined' && window.Worker && window.Blob) {
        const check = new Blob(["(", function(){
            self.onmessage = function(e) {
              const interval = e.data;
            let startTime = Date.now();
            while(true) {
                if (Date.now() - startTime >= interval) {
                  startTime = Date.now();
                self.postMessage(Date.now());
              }
            }
          }
        }.toString(), ")()"], { type: "text/javascript" });
        const worker = new Worker(window.URL.createObjectURL(check));
        worker.onmessage = func;
        worker.postMessage(interval);
                return worker;
    } else {
        console.log('Your environment is not supported');
    }
}

const myClearInterval = config => { config.terminate() }

ethannnli
858 声望360 粉丝