有没有比setTimeout更准确的方法来创建Javascript计时器?


68

一直困扰我的是setTimeout()Javascript中的方法多么不可预测。

以我的经验,计时器在很多情况下都非常不准确。所谓不准确,是指实际延迟时间或多或少地相差250-500ms。尽管这不是很长的时间,但是使用它来隐藏/显示UI元素时,时间显然是显而易见的。

是否有任何技巧可以确保setTimeout()准确执行(无需借助外部API),或者这是一个失败的原因?

Answers:


74

是否有任何技巧可以确保setTimeout()准确执行(无需借助外部API),或者这是一个失败的原因?

不,不。您不会通过它获得任何接近完美准确的计时器的功能setTimeout()-尚未为此设置浏览器。但是,您也不需要依靠它来计时。多数动画库是在几年前弄清楚的:您使用设置了回调setTimeout(),但根据的值确定需要执行的操作(new Date()).milliseconds(或等效)。这使您可以在较新的浏览器中利用更可靠的计时器支持,同时在较旧的浏览器上仍可正常运行。

它还可以避免使用太多计时器!这很重要:每个计时器都是一个回调。每个回调都执行JS代码。在执行JS代码时,浏览器事件(包括其他回调)将被延迟或丢弃。回调完成后,其他回调必须与其他浏览器事件竞争才能执行。因此,一个在该时间间隔内处理所有待处理任务的计时器要比两个时间间隔一致的计时器要好,并且(对于短超时)要比两个重叠超时的计时器要好!

摘要:停止使用setTimeout()实现“一个计时器/一个任务”的设计,并使用实时时钟来平滑UI动作。


如何从工作程序调用setTimeout()。它不能减少渲染引起的阻塞吗?
hidarikani 2014年

不能保证,@ hidarikani。FWIW,这在本机代码中可能同样是一个问题,在浏览器中,您没有能力要求高分辨率计时器和实时延迟。不是您想要的;那是浪费CPU的秘诀,并且您应该假设至少在某些情况下,浏览器和OS将尽其所能来保持电池寿命并为此目的牺牲计时器精度。
Shog9 2014年

我认为这个答案是错的,现在实际上,自从我们有了requestAnimationFrame现在......又见此答案克里斯。这不是一个简单的解决方案,但是它满足了OP不需要外部API的标准。
Stijn de Witt 2015年

1
requestAnimationFrame提供更高的精度,但不一定提供更高的精度@StijndeWitt-您仍然受浏览器的摆布,这可能会因其他处理甚至在回调中实现时做出的选择而停滞。克里斯的技巧很好,但是如果您想确保逻辑尽可能地接近设定的时间执行,您仍然需要根据检索到的时间值来决定什么是到期时间。与setTimeout相比,requestAnimationFrame对何时调用的保证甚至更少。另请参阅我以前给hidarikani的笔记。
Shog9'2

“ requestAnimationFrame提供的保证更少”这是非常好的一点!谢谢
Stijn de Witt

33

REF; http://www.sitepoint.com/creating-accurate-timers-in-javascript/

这个网站在很大程度上帮助了我。

您可以使用系统时钟来补偿计时器的不准确性。如果您将计时功能作为一系列setTimeout调用(每个实例都调用下一个实例)来运行,则要确保其准确性,您要做的就是精确地确定其准确性,并从下一次迭代中减去该差值:

var start = new Date().getTime(),  
    time = 0,  
    elapsed = '0.0';  
function instance()  
{  
    time += 100;  
    elapsed = Math.floor(time / 100) / 10;  
    if(Math.round(elapsed) == elapsed) { elapsed += '.0'; }  
    document.title = elapsed;  
    var diff = (new Date().getTime() - start) - time;  
    window.setTimeout(instance, (100 - diff));  
}  
window.setTimeout(instance, 100);  

这种方法将使漂移最小化,并将不准确度降低90%以上。

它解决了我的问题,希望对您有所帮助


@Mr_Chimp,为什么elapsed在代码中需要变量?
2014年

@AntonRudeshko Huh ...好问题。我的代码改编自上面的链接。认为那只是一个没用的剩菜
Mr_Chimp

doTimer那篇文章结尾的功能正是我所需要的。超级准确。
卢克

10

我不久前遇到了类似的问题,并提出了一种requestAnimationFrame与performance.now()相结合的方法,该方法非常有效。

我现在可以使计时器精确到小数点后12位:

    window.performance = window.performance || {};
    performance.now = (function() {
        return performance.now       ||
            performance.mozNow    ||
            performance.msNow     ||
            performance.oNow      ||
            performance.webkitNow ||
                function() {
                    //Doh! Crap browser!
                    return new Date().getTime(); 
                };
        })();

http://jsfiddle.net/CGWGreen/9pg9L/



4

shog9的答案几乎是我要说的,尽管我会添加以下有关UI动画/事件的内容:

如果您有一个应该滑到屏幕上,向下扩展,然后淡入其内容的框,请不要尝试将所有三个事件分开并设置延迟时间以使其一次又一次触发-使用回调,因此请使用一次回调滑动完成第一个事件后,它将调用扩展器,一旦完成,它将调用推子。jQuery可以轻松做到这一点,而且我相信其他库也可以做到。


3

如果您习惯于setTimeout()快速让浏览器生成内容,以便它的UI线程可以赶上它需要完成的任何任务(例如更新选项卡,或者不显示“长时间运行的脚本”对话框),那么有一个名为Efficient的新API脚本Yielding,又名,setImmediate()可能对您更好。

setImmediate()与的操作非常相似setTimeout(),但如果浏览器没有其他操作,它可能会立即运行。在许多使用setTimeout(..., 16)or或setTimeout(..., 4)or的情况下setTimeout(..., 0)(例如,您希望浏览器运行任何未完成的UI线程任务并且不显示“长时间运行的脚本”对话框),只需将您的替换setTimeout()setImmediate(),并删除第二个(毫秒)参数即可。

与之不同的setImmediate()是,它基本上是产量。如果浏览器有时间在UI线程上执行操作(例如,更新选项卡),它将在返回回调之前执行此操作。但是,如果浏览器已经完成了所有工作,则在setImmediate()将基本上没有延迟地运行。

不幸的是,目前只有IE9 +支持此功能,因为有些支持其他浏览器供应商。

但是,如果您想使用它,并希望其他浏览器在某个时候实现它,则可以使用一个很好的polyfill

如果setTimeout()用于动画,请最好选择requestAnimationFrame,因为代码将与监视器的刷新率同步运行。

如果您使用setTimeout()较慢的节奏(例如每300毫秒一次),则可以使用类似于user1213320建议的解决方案,在该解决方案中,您监视自上次时间戳开始运行了多长时间,并补偿了任何延迟。一项改进是,您可以使用新的“高分辨率时间”界面(aka window.performance.now()),而不是Date.now()在当前时间获得大于毫秒的分辨率。


2

您需要“提高”目标时间。某些尝试和错误将是必要的,但本质上是这样。

设置超时时间以在所需时间之前100毫秒完成

使超时处理程序函数如下所示:

calculate_remaining_time
if remaining_time > 20ms // maybe as much as 50
  re-queue the handler for 10ms time
else
{
  while( remaining_time > 0 ) calculate_remaining_time;
  do_your_thing();
  re-queue the handler for 100ms before the next required time
}

但是您的while循环仍然会被其他进程打断,因此仍然不够完美。


假设您需要在圆点上敲击确切的时钟时间这不是一个糟糕的主意。但是,如果您只是在一个一致的间隔之后,则可以通过简单地将运行中的超时值调整为仅比间隔时间少一些,而不花费处理时间来达到类似的结果。即使对于提醒或滴答滴答的时钟,大多数情况下15ms的精度也可能足够好。
Shog9'9

我唯一需要这种准确性的时间是尝试实现音乐节拍。但是Javascript计时器并不真正适合这项工作。即使采用这种设计,滴答声也很不稳定,而且并不真正适合节拍器。
Noel Walters

2

这是演示Shog9的建议的示例。这会在6秒钟内顺利填充jquery进度栏,然后在填充后重定向到另一个页面:

var TOTAL_SEC = 6;
var FRAMES_PER_SEC = 60;
var percent = 0;
var startTime = new Date().getTime();

setTimeout(updateProgress, 1000 / FRAMES_PER_SEC);

function updateProgress() {
    var currentTime = new Date().getTime();

    // 1000 to convert to milliseconds, and 100 to convert to percentage
    percent = (currentTime - startTime) / (TOTAL_SEC * 1000) * 100;

    $("#progressbar").progressbar({ value: percent });

    if (percent >= 100) {
        window.location = "newLocation.html";
    } else {
        setTimeout(updateProgress, 1000 / FRAMES_PER_SEC);
    }                 
}

setTimer()函数从何而来?
蒂姆·霍尔曼

@TimHallman我想我的意思是setTimeout()。我将进行相应的编辑。
教务长

1

这是我为我的音乐项目制作的一个计时器,用于执行此操作。在所有设备上都准确的计时器。

var Timer = function(){
  var framebuffer = 0,
  var msSinceInitialized = 0,
  var timer = this;

  var timeAtLastInterval = new Date().getTime();

  setInterval(function(){
    var frametime = new Date().getTime();
    var timeElapsed = frametime - timeAtLastInterval;
    msSinceInitialized += timeElapsed;
    timeAtLastInterval = frametime;
  },1);

  this.setInterval = function(callback,timeout,arguments) {
    var timeStarted = msSinceInitialized;
    var interval = setInterval(function(){
      var totaltimepassed = msSinceInitialized - timeStarted;
      if (totaltimepassed >= timeout) {
        callback(arguments);
        timeStarted = msSinceInitialized;
      }
    },1);

    return interval;
  }
}

var timer = new Timer();
timer.setInterval(function(){console.log("This timer will not drift."),1000}


原来您的代码每100秒漂移约116毫秒:我在调用timer.setInterval()之前添加了以下内容:<code> var startTime = new Date()。getTime(); </ code>并添加:<code> var现在= new Date()。getTime(); var ms =(now-startTime)+'ms'; </ code>在回调中,结果是:此计时器将漂移。起始毫秒:100116毫秒
TriumphST

0

讨厌这么说,但我认为没有办法缓解这种情况。我认为这取决于客户端系统上,不过,这样更快的JavaScript引擎或机器可能使其稍微更准确。


我有一台双核2.8 Ghz机器,计时器仍然非常不准确。以我的经验,CPU速度并不重要。
Dan Herbert

0

以我的经验,这是徒劳的,即使我认识到js的最短合理时间约为32-33 ms。...


0

这里肯定有一个限制。为了给您一些视角,Google刚刚发布的Chrome浏览器足够快,可以在15-20毫秒内执行setTimeout(function(){},0),而较早的Javascript引擎执行该功能要花费数百毫秒的时间。尽管setTimeout使用毫秒,但是目前没有JavaScript虚拟机可以执行这种精度的代码。


我听说Chrome是第一个如此精确的浏览器,但是我仍然无法对其进行测试。
丹·赫伯特

2
有点麻烦了:Windows上的标准计时器间隔为15ms,因此大多数Windows上运行的浏览器中的计时器间隔为15ms 。Chrome显然正在使用更准确的硬件计时器来提供更好的粒度。JS执行速度是一个单独的问题。
Shog9,

Chrome已经为此解决了一些问题-提高setTimeout分辨率会破坏某些假定为15ms分辨率的网站,并破坏Windows的电池寿命(Windows体系结构显然会影响分辨率的计时器)
olliej

0

Dan,根据我的经验(包括在JavaScript中实现SMIL2.1语言的实现,其中涉及到时间管理),我可以向您保证,您实际上不需要setTimeout或setInterval的高精度。

但是,重要的是在排队时执行setTimeout / setInterval的顺序-始终可以完美运行。


尽管我从不需要真正的高精度计时,但是当我为依赖于UI的某些功能设置的延迟稍有减少时,这可能会很麻烦。它不会破坏任何东西,但是由于它有点不一致,因此可以注意到它。
丹·赫伯特

Shog9提供了我用简单语句包装的内容的完整见解。是的,不是“浏览器”或“ javascript实现”不能确保时间,而是操作系统,人员,世界等。因此,取决于“发生”,然后取决于“定时”。
谢尔盖·伊林斯基,

0

JavaScript超时的实际限制为10到15毫秒(除非您实际执行185毫秒,否则我不确定您要做什么才能获得200毫秒)。这是由于Windows具有15ms的标准计时器分辨率,因此,做得更好的唯一方法是使用Windows的高分辨率计时器,这是系统范围的设置,因此可以与系统上的其他应用程序连接,并且可以减少电池寿命(Chrome英特尔在此问题上的错误)。

实际的10-15毫秒标准是由于人们在网站上使用0毫秒超时,然后以一种假设10 -15毫秒超时的方式进行编码(例如,js游戏假设60 fps,但要求0毫秒/帧,没有增量逻辑,因此游戏/网站/动画的速度要比预期快几个数量级)。为此,即使在没有Windows计时器问题的平台上,浏览器也将计时器分辨率限制为10ms。


1
需要说明的是,Chrome实际上会接受0ms超时(而是在可能的最短时间执行它们)。
2008年

0

这是我用的。由于是JavaScript,因此我将同时发布Frontend和node.js解决方案:

对于这两种方法,我都建议使用相同的十进制舍入函数,我极力建议您保持十足的距离,原因如下:

const round = (places, number) => +(Math.round(number + `e+${places}`) + `e-${places}`)

places-四舍五入的小数位数应该是安全的,并且应该避免浮点数的任何问题(某些数字,例如1.0000000000005〜可能是有问题的)。我花了一些时间研究舍入由高分辨率计时器提供的十进制数的最佳方法,该计时器已转换为毫秒。

that + symbol -这是一元运算符,可将操作数转换为数字,实际上与 Number()

浏览器

const start = performance.now()

// I wonder how long this comment takes to parse

const end = performance.now()

const result = (end - start) + ' ms'

const adjusted = round(2, result) // see above rounding function

node.js

// Start timer
const startTimer = () => process.hrtime()

// End timer
const endTimer = (time) => {
    const diff = process.hrtime(time)
    const NS_PER_SEC = 1e9
    const result = (diff[0] * NS_PER_SEC + diff[1])
    const elapsed = Math.round((result * 0.0000010))
    return elapsed
}

// This end timer converts the number from nanoseconds into milliseconds;
// you can find the nanosecond version if you need some seriously high-resolution timers.

const start = startTimer()

// I wonder how long this comment takes to parse

const end = endTimer(start)

console.log(end + ' ms')

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.