setTimeout不准时?你大意了

随着时间的推移, setTimeout 实际执行的时间和理想的时间差值会越来越大,这就不是我们预期的样子。类比真实的场景,对于一些倒计时以及动画来说都会造成时间的偏差都是不理想的。

为什么 setTimeout 会不准时呢?

因为我们的代码往往并不是只有一个 setTimeout,大多数会遇到以下情况:
在这里插入图片描述
总结来说,因为浏览器页面是有消息队列和事件循环来驱动的,创建一个 setTimeout 的时候是将它推进了一个队列,并没有立即执行,只有本轮宏任务执行完,才会去检查当前的消息队列是否有有到期的任务。

方法一:while

想得到准确的,我们第一反应就是如果我们能够主动去触发,获取到最开始的时间,以及不断去轮询当前时间,如果差值是预期的时间,那么这个定时器肯定是准确的,那么用 while 可以实现这个功能。

function timer(time) {
    
    
    const startTime = Date.now();
    while(true) {
    
    
        const now = Date.now();
        if(now - startTime >= time) {
    
    
            console.log('误差', now - startTime - time);
            return;
        }
    }
}
timer(5000);

打印:误差 0

显然这样的方式很精确,但是我们知道 js 是单线程运行,使用这样的方式强行霸占线程会使得页面进入卡死状态,这样的结果显然是不合适的。

方法二:Web Worker

那么既然无法在当前主线程避免这个误差,我们能否另开一个线程去处理呢?当然可以,JavaScript 也提供给我们这样一个能力,通过 Web Worker 我们就可以在另一个线程来运行我们的代码。

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面

// main.js
var myWorker = new Worker('worker.js');

// 监听 worker
myWorker.onmessage = function(e) {
    
    
  result.textContent = e.data;
  console.log('Message received from worker');
}
first.onchange = function() {
    
    
  // 向 worker 发送数据
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

// worker.js
onmessage = function(e) {
    
    
  // 接受主线程的数据
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
  console.log('Posting message back to main script');
  // 向主线程发送数据
  postMessage(workerResult);
}

那么接下来我们就要加 worker 和 while 相结合,以下为创建 worker 部分

// worker生成器
const createWorker = (fn, options) => {
    
    
    const blob = new Blob(['(' + fn.toString() + ')()']);
    const url = URL.createObjectURL(blob);
    if (options) {
    
    
        return new Worker(url, options);
    }
    return new Worker(url);
} 
// worker 部分
const worker = createWorker(function () {
    
    
    onmessage = function (e) {
    
    
        const date = Date.now();
        while (true) {
    
    
            const now = Date.now();
            if(now - date >= e.data) {
    
    
                postMessage(1);
                return;
            }
        }
    }
})

我们通过在 worker 中写入一个 while 循环,当达到我们的预取时间的时候,再向主线程发送一个完成事件,就不会因为主线程的其他代码的干扰而造成数据不准的情况。

let isStart = false;
function timer() {
    
    
    worker.onmessage = function (e) {
    
    
       cb()
        if (isStart) {
    
    
            worker.postMessage(speed);
        } 
    }
    worker.postMessage(speed);
}

执行的时间和理想的时间非常相近,而那细微的差异应该就是线程通讯耗时。

虽然我们用 Web Worker 修复时间看似被解决了。但是一方面, worker 线程会被 while 给占用,导致无法接受到信息,多个定时器无法同时执行,另一方面,由于 onmessage 还是属于事件循环内,如果主线程有大量阻塞还是会让时间越差越大,因此这并不是个完美的方案。

方法三:requestAnimationFrame

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,回调函数执行次数通常是每秒60次,也就是每16.7ms 执行一次,但是并不一定保证为 16.7 ms。

// 模拟代码
function setTimeout2 (cb, delay) {
    
    
    let startTime = Date.now()
    loop()
  
    function loop () {
    
    
      const now = Date.now()
      if (now - startTime >= delay) {
    
    
        cb();
        return;
      }
      requestAnimationFrame(loop)
    }
}

由于 16.7 ms 间隔执行,在使用间隔很小的定时器,很容易导致时间的不准确。

略微加剧了误差的增加,因此这种方案仍然不是一种好的方案。

方法四:setTimeout 系统时间补偿

这个方案是在 stackoverflow 看到的一个方案,我们来看看此方案和原方案的区别

原方案:
在这里插入图片描述
setTimeout系统时间补偿:
在这里插入图片描述
当每一次定时器执行时后,都去获取系统的时间来进行修正,虽然每次运行可能会有误差,但是通过系统时间对每次运行的修复,能够让后面每一次时间都得到一个补偿。

function timer() {
    
    
   var speed = 500,
   counter = 1, 
   start = new Date().getTime();
   
   function instance()
   {
    
    
    var ideal = (counter * speed),
    real = (new Date().getTime() - start);
    
    counter++;

    var diff = (real - ideal);
    form.diff.value = diff;

    window.setTimeout(function() {
    
     instance(); }, (speed - diff)); // 通过系统时间进行修复

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

好了我们最后来总结一下4种方案的优缺点:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_42224055/article/details/116492329