最近有粉丝去字节,被面试官问这个问题问我。听起来有点意思,所以就研究了一下,可能研究的过程和结果不一定是最好的,但我们还是记录下来,给大家提供一些帮助。得到这道题,假设有这么一个场景,我们需要用setTimeout做一个动画,我们需要控制它的频率,50ms运行一次。首先我们先上图看看setTimeout的表现。运行代码如下。一个计数器用来记录每次setTimeout的调用,设置的间隔*计数次数等于理想状态下的延时。使用以下示例来检查我们的计时器的准确性。functiontimer(){varspeed=50,//设置间隔counter=1,//计数start=newDate().getTime();functioninstance(){varideal=(counter*speed),real=(newDate().getTime()-start);counter++;form.ideal.value=ideal;//记录理想值form.real.value=real;//记录真实值vardiff=(real-ideal);form.diff.value=diff;//区别window.setTimeout(function(){instance();},speed);};window.setTimeout(function(){instance();},speed);}timer();而如果我们在setTimeout的执行期间添加一些额外的代码逻辑,再来看区别。...window.setTimeout(function(){instance();},speed);for(varx=1,i=0;i<10000000;i++){x*=(i+1);}}...可以看出,这大大加剧了错误。可以看出,随着时间的推移,setTimeout的实际执行时间与理想时间的差距会越来越大,这并不是我们所期望的。类比真实场景,对于一些倒计时和动画会造成时间偏差并不理想。那么,从这个现象来看,为什么setTimeout没有准时呢?因为我们的代码往往不会只有一个setTimeout,所以大多会遇到以下几种情况。具体要从浏览器的事件循环说起,但是关于事件循环的文章太多了,文中就不详细讲解了。视频https://www.youtube.com/watch?v=8aGhZQkoFbQ(国内视频https://www.bilibili.com/video/av456657611/)建议看国外中英文字幕,国内翻译准确度一般相关文章https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/总结说因为浏览器页面是由消息队列和事件循环驱动的,所以在创建setTimeout的时候,是往前推一个队列的,不是立即执行。只有当本轮宏任务执行完毕后,才会检查当前消息队列是否有到期任务。接下来我就用4来这样探索。在想要准确的同时,我们的第一反应是,如果能主动触发,拿到初始时间,不断轮询当前时间,如果差的是期望的时间,那么这个定时器一定是准确的,那么这个功能就可以实现一会儿。理解起来也很简单:代码如下:functiontimer(time){conststartTime=Date.now();while(true){constnow=Date.now();if(now-startTime>=time){console.log('错误',now-startTime-time);返回;}}}计时器(5000);print:error0显然这种方法是非常准确的,但是我们知道js是单线程运行的,使用这种方法强行占用线程会让页面进入卡死状态,这样的结果显然是不合适的。WebWorker,既然在当前主线程中无法避免这个错误,那我们是否可以另开一个线程来处理呢?当然,JavaScript也为我们提供了这样的能力。通过WebWorker,我们可以在另一个线程中运行我们的应用程序。代码。WebWorker为Web内容提供了一种在后台线程中运行脚本的简单方法。线程可以在不干扰用户界面的情况下执行任务。--来自MDN的worker的简单示例//main.jsvarmyWorker=newWorker('worker.js');//监控workermyWorker.onmessage=function(e){result.textContent=e.data;console.log('Messagereceivedfromworker');}first.onchange=function(){//发送数据给workermyWorker.postMessage([first.value,second.value]);console.log('Messagepostedtoworker');}//worker.jsonmessage=function(e){//从主线程接收数据console.log('Messagereceivedfrommainscript');varworkerResult='Result:'+(e.data[0]*e.data[1]);console.log('Postingmessagebacktomainscript');//向主线程发送数据postMessage(workerResult);}然后我们需要添加worker和while的组合,下面是创建worker的部分(fn,options)=>{constblob=newBlob(['('+fn.toString()+')()']);consturl=URL.createObjectURL(blob);if(options){returnnewWorker(url,选项);}returnnewWorker(url);}//worker部分constworker=createWorker(function(){onm??essage=function(e){constdate=Date.now();while(true){constnow=Date.now();if(now-date>=e.data){postMessage(1);return;}}}})我们在worker中写了一个while循环,当我们的预取时间到了,向主线程发送一个完成事件,不会因为主线程其他代码的干扰导致数据不准确letisStart=false;functiontimer(){worker.onmessage=function(e){cb()if(isStart){worker.postMessage(speed);}}worker.postMessage(speed);}我们来看看实际效果。我们可以看到执行时间已经很接近理想时间了,略有不同的应该是线程通信比较耗时。我们来看一下添加额外代码逻辑的情况。...if(isStart){worker.postMessage(speed);}for(varx=1,i=0;i<10000000;i++){x*=(i+1);}...时间明显增加,但增加速度很慢。尽管我们与WebWorkers的修复时间似乎已经解决了。但是一方面,工作线程会被while占用,导致无法接收信息,多个定时器无法同时执行。另一方面,由于onmessage还是属于eventloop,如果主线程有很多阻塞,还是会让timeworse变大,所以不是一个完美的解决方案。requestAnimationFrame先看它的定义window.requestAnimationFrame()告诉浏览器你要执行一个动画,要求浏览器在下次重绘前调用指定的回调函数来更新动画。该方法需要传入一个回调函数作为参数。该回调函数将在浏览器下一次重绘之前执行。回调函数的执行次数通常为每秒60次,即每16.7ms执行一次,但不一定保证是16.7ms。我们也可以尝试模拟setTimeout。//模拟代码函数setTimeout2(cb,delay){letstartTime=Date.now()loop()functionloop(){constnow=Date.now()if(now-startTime>=delay){cb();return;}requestAnimationFrame(loop)}}发现由于16.7ms的间隔执行,使用间隔较小的定时器时,容易造成时间不准。让我们看看引入额外代码的效果。...window.setInterval2(function(){instance();},speed);}for(varx=1,i=0;i<10000000;i++){x*=(i+1);}...稍微加剧了错误的增加,所以这个解决方案仍然不是一个好的解决方案。setTimeout系统时间补偿这个方案是在stackoverflow看到的方案。我们来看看这个方案和原方案的区别。原方案setTimeout系统时间补偿每次执行定时器都会获取系统时间进行校正,虽然每次运行都可能存在误差,但是通过对每次运行的系统时间的修复,后续的每一次时间都可以得到补偿。functiontimer(){varspeed=500,counter=1,start=newDate().getTime();functioninstance(){varreal=(counter*speed),ideal=(newDate().getTime()-start);counter++;vardiff=(ideal-real);form.diff.value=diff;window.setTimeout(function(){instance();},(speed-diff));//按系统时间修复};window.setTimeout(function(){instance();},speed);}我们来看看添加额外代码逻辑的情况。还是很稳定的,所以通过系统的时间补偿,可以让我们的setTimeout更加准时,至此我们就完成了如何让setTimeout准时的探索。好了,最后总结一下四种方案的优缺点whileWebWorkerrequestAnimationFramesetTimeout系统时间补偿whileWebWorkerrequestAnimationFramesetTimeout系统时间补偿精度高高低高主线程阻塞一般不阻塞非阻塞评分??????????????????????下次见~参考https://segmentfault.com/q/1010000013909430https://stackoverflow.com/questions/196027/is-there-a-more-accurate-way-to-create-a-javascript-timer-than-settimeout
