如何让前端 setTimeout 更准时?

357 阅读4分钟

前言

在前端开发中,setTimeout 是一个常用的函数,用于在指定的时间后执行一段代码。然而,setTimeout 的执行时间是否真的准确呢?本文将通过实验和分析,带你一起探究 setTimeout 的准确性。

图片

setTimeout 的机制

setTimeout 是 JavaScript 中的一个宏任务,当我们调用 setTimeout 时,实际上只是将任务放入主线程队列的时间,而不是任务实际执行的时间。主线程上待处理的任务数量会影响回调的执行时间。这意味着,setTimeout 的回调函数并不是在指定的时间点准时执行,而是要等待主线程上的其他任务处理完成后才会执行。

实验与分析

为了验证 setTimeout 的准确性,我们可以通过一个简单的实验来观察其实际执行时间与理想时间的差异。

function time () {
    var speed = 50// 间隔
    count = 1 // 计数
    start = new Date().getTime();

    function instance() {
        var ideal=(count * speed),
        real = (new Date().getTime() - start);

        count++;
        console.log(count + '理想值------------------------:', ideal); // 记录理想值
        console.log(count + '真实值------------------------:', real); // 记录真实值

        var diff = (real - ideal);
        console.log(count + '差值------------------------:', diff); // 差值

        // 小于5执行
        if (count < 5) {
            window.setTimeout(function(){
                instance();
            }, speed);
        };

    };

    window.setTimeout(function () {
        instance();
    }, speed);
};

在上述代码中,我们设置了一个间隔为 50 毫秒的定时器,并记录了每次执行的理想值、真实值以及它们之间的差值。通过观察输出结果,我们可以发现,随着时间的推移,实际执行时间与理想时间之间的差距会不断扩大。

为了进一步验证,我们可以在 setTimeout 执行之前加入额外的代码逻辑,比如一个循环计算,然后再观察差值的变化。实验结果表明,这会大大增加误差,进一步说明了主线程上任务的执行会影响 setTimeout 的准确性。

提高 setTimeout 准确性的方法

使用 requestAnimationFrame

requestAnimationFrame 是浏览器用于动画渲染的一个方法,它会在浏览器下一次重绘之前调用指定的回调函数。我们可以利用 requestAnimationFrame 来模拟 setTimeout,以提高定时器的准确性。

function setTimeout2(cb, delay) {
    const startTime = Date.now();

    function loop() {
        const now = Date.now();
        if (now - startTime >= delay) {
            cb();
        } else {
            requestAnimationFrame(loop);
        }
    }

    requestAnimationFrame(loop);
};

然而,通过实验发现,使用 requestAnimationFrame 并不能完全消除误差,因为它的回调执行次数通常受浏览器刷新率的限制,并不一定能保证精确的时间间隔。

使用 while 循环

如果我们能够主动去触发定时器,通过不断轮询当前时间,当差值达到预期时间时执行回调,理论上可以实现更准确的定时器。使用 while 循环可以实现这一功能,但由于其会导致 CPU 占用率极高,页面进入卡死状态,因此并不适合实际应用。

function time2(time) {
    const startTime = Date.now();

    function checkTime() {
        const now = Date.now();
        if (now - startTime >= time) {
            console.log('误差', now - startTime - time);
        } else {
            setTimeout(checkTime, 1); // 每毫秒检查一次
        }
    }

    checkTime();
}

time2(5000);

setTimeout 系统时间补偿

在 Stack Overflow 上看到的一个方案是,在每次定时器执行后,都获取系统时间进行修正。虽然每次运行可能会有误差,但通过系统时间对每次运行的补偿,可以使后续的时间更加准确。

function time () {
    var speed = 50// 间隔
    count = 1 // 计数
    start = new Date().getTime();

    function instance() {
        var ideal=(count * speed),
        real = (new Date().getTime() - start);

        count++;
        console.log(count + '理想值------------------------:', ideal); // 记录理想值
        console.log(count + '真实值------------------------:', real); // 记录真实值

        var diff = (real - ideal);
        console.log(count + '差值------------------------:', diff); // 差值

        // 5次后不再执行
        if (count < 5) {
            window.setTimeout(function(){
                instance();
            }, (speed - diff));
        };
    };

    window.setTimeout(function () {
        instance();
    }, speed);
};

通过这种方式,我们可以看到误差得到了有效控制,变得微乎其微,几乎可以忽略不计。

结论

通过多次实验和尝试不同的方法,我们发现 setTimeout 并不完全可靠,其执行时间会受到主线程上任务数量的影响。然而,通过系统时间补偿的方法,可以使 setTimeout 的执行更加准时,误差几乎可以忽略不计。在实际应用中,如倒计时和动画等场景,我们可以根据具体需求选择合适的定时器实现方式,以达到最佳的效果。