Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何实现准时的 setTimeout #3

Open
silverWolf818 opened this issue Nov 26, 2023 · 0 comments
Open

如何实现准时的 setTimeout #3

silverWolf818 opened this issue Nov 26, 2023 · 0 comments
Labels
javascript javascript

Comments

@silverWolf818
Copy link
Owner

silverWolf818 commented Nov 26, 2023

背景

我们都知道 setTimeout 是不准的。因为 setTimeout 是一个宏任务,它的指定时间指的是:进入主线程的时间。

setTimeout(callback, 进入主线程的时间)

所以什么时候可以执行 callback,需要看 主线程前面还有多少任务待执行。

我们可以通过这个场景来进行演示:

image

运行代码如下,通过一个计数器来记录每一次 setTimeout 的调用,而设定的间隔 * 计数次数,就等于理想状态下的延迟,通过以下例子来查看我们计时器的准确性。

function timer() { 
   var speed = 50, // 设定间隔 
   counter = 1,  // 计数 
   start = new Date().getTime(); 
    
   function instance() 
   { 
    var ideal = (counter * speed), 
    real = (new Date().getTime() - start); 
     
    counter++; 
    form.ideal.value = ideal; // 记录理想值 
    form.real.value = real;   // 记录真实值 
 
    var diff = (real - ideal); 
    form.diff.value = diff;  // 差值 
 
    window.setTimeout(function() { instance(); }, speed); 
   }; 
    
   window.setTimeout(function() { instance(); }, speed); 
} 
timer(); 

我们如果在 setTimeout 还未执行期间加入一些额外的代码逻辑,再来看看这个差值。

... 
window.setTimeout(function() { instance(); }, speed); 
for(var x=1, i=0; i<10000000; i++) { x *= (i + 1); } 
} 
... 

image

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

那么,从这个现象来看一下,为什么 setTimeout 会不准时呢?

因为我们的代码往往并不是只有一个 setTimeout,大多数会遇到以下情况。

image

requestAnimationFrame

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。

该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,回调函数执行次数通常是每秒60次,也就是每16.7ms 执行一次,但是并不一定保证为 16.7 ms。

我们也可以尝试一下将它来模拟 setTimeout。

function timer(cb, delay) { 
    let startTime = Date.now() 
    loop() 
   
    function loop () { 
      const now = Date.now() 
      if (now - startTime >= delay) { 
        cb(); 
        return; 
      } 
      requestAnimationFrame(loop) 
    } 
} 

image

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

image

再看看额外代码的引入效果。

... 
 window.setInterval2(function () { instance(); }, speed); 
} 
for (var x = 1, i = 0; i < 10000000; i++) { x *= (i + 1); } 
... 

image

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

while

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

理解起来也很简单:

image
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 我们就可以在另一个线程来运行我们的代码。

// 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); 
} 

我们来看一下实际的效果:

5dab934dd2ef6700fc7de7b684f3b154

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

setTimeout 系统时间补偿

这个方案是在 stackoverflow 看到的一个方案

setTimeout系统时间补偿

image

当每一次定时器执行时后,都去获取系统的时间来进行修正,虽然每次运行可能会有误差,但是通过系统时间对每次运行的修复,能够让后面每一次时间都得到一个补偿。

function timer() { 
   var speed = 500, 
   counter = 1,  
   start = new Date().getTime(); 
    
   function instance() 
   { 
    var real = (counter * speed), 
    ideal = (new Date().getTime() - start); 
     
    counter++; 
 
    var diff = (ideal - real); 
    form.diff.value = diff; 
 
    window.setTimeout(function() { instance(); }, (speed - diff)); // 通过系统时间进行修复 
 
   }; 
    
   window.setTimeout(function() { instance(); }, speed); 
} 

5dab934dd2ef6700fc7de7b684f3b154

因此通过系统的时间补偿,能够让我们的 setTimeout 变得更加准时,至此我们完成了如何让 setTimeout 准时的探索。

@silverWolf818 silverWolf818 added the javascript javascript label Nov 26, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
javascript javascript
Projects
None yet
Development

No branches or pull requests

1 participant