74

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

根据我的经验,计时器在很多情况下都非常不准确。不准确,我的意思是实际延迟时间似乎或多或少地变化了 250-500 毫秒。虽然这不是很长的时间,但当使用它来隐藏/显示 UI 元素时,时间可能会很明显。

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

4

16 回答 16

77

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

没有也没有。你不会得到任何接近完美准确计时器的东西setTimeout()- 浏览器没有为此设置。但是,您也不需要依赖它来计时。大多数动画库在几年前就发现了这一点:您使用 设置回调,但根据(或等价物)setTimeout()的值确定需要做什么。(new Date()).milliseconds这使您可以在较新的浏览器中利用更可靠的计时器支持,同时在较旧的浏览器上仍能正常运行。

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

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

于 2008-10-12T21:32:23.833 回答
34

.

参考;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% 以上。

解决了我的问题,希望对你有帮助

于 2012-03-06T01:52:09.717 回答
11

不久前我遇到了类似的问题,并提出了一种结合requestAnimationFrameperformance.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/

于 2013-04-19T11:17:51.247 回答
5

如果您需要在给定的时间间隔内获得准确的回调,此要点可能会对您有所帮助:

https://gist.github.com/1185904

function interval(duration, fn){
  var _this = this
  this.baseline = undefined
  
  this.run = function(){
    if(_this.baseline === undefined){
      _this.baseline = new Date().getTime()
    }
    fn()
    var end = new Date().getTime()
    _this.baseline += duration
 
    var nextTick = duration - (end - _this.baseline)
    if(nextTick<0){
      nextTick = 0
    }
    
    _this.timer = setTimeout(function(){
      _this.run(end)
    }, nextTick)
  }

  this.stop = function(){
    clearTimeout(_this.timer)
  }
}
于 2011-09-01T10:36:11.683 回答
4

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

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

于 2008-10-12T23:04:25.880 回答
3

如果您使用setTimeout()快速屈服于浏览器以便它的 UI 线程可以赶上它需要执行的任何任务(例如更新选项卡或不显示长时间运行脚本对话框),那么有一个名为Efficient的新 API Script Yielding,又名,setImmediate()它可能对你更好一些。

setImmediate()操作与 非常相似setTimeout(),但如果浏览器无事可做,它可能会立即运行。在许多使用setTimeout(..., 16)or setTimeout(..., 4)or的情况下setTimeout(..., 0)(即您希望浏览器运行任何未完成的 UI 线程任务而不显示长时间运行脚本对话框),您可以简单地替换您的setTimeout()with setImmediate(),删除第二个(毫秒)参数。

不同的setImmediate()是,它基本上是一个收益;如果浏览器有时间在 UI 线程上执行(例如,更新选项卡),它会在返回回调之前执行此操作。但是,如果浏览器已经全部赶上它的工作,那么指定的回调setImmediate()将基本上毫无延迟地运行。

不幸的是,它目前仅在 IE9+ 中受支持,因为其他浏览器供应商对此有所回击

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

如果您使用setTimeout()动画requestAnimationFrame是您最好的选择,因为您的代码将与显示器的刷新率同步运行。

如果您使用setTimeout()较慢的节奏,例如每 300 毫秒一次,您可以使用类似于 user1213320 建议的解决方案,在该解决方案中,您可以监控从计时器运行的最后一个时间戳开始的时间并补偿任何延迟。一项改进是您可以使用新的高分辨率时间接口 (aka window.performance.now()) 而不是Date.now()获得当前时间大于毫秒的分辨率。

于 2014-01-14T15:38:16.807 回答
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 循环仍然会被其他进程打断,所以它仍然不完美。

于 2010-09-06T14:20:04.327 回答
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);
    }                 
}
于 2013-03-08T01:16:33.053 回答
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}

于 2014-12-04T16:33:57.643 回答
0

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

于 2008-10-12T20:45:58.453 回答
0

根据我的经验,即使我认为 js 执行的最小合理时间约为 32-33 毫秒,这也是徒劳的。...

于 2008-10-12T20:46:54.083 回答
0

这里肯定有一个限制。给你一些观点,谷歌刚刚发布的 Chrome 浏览器速度足够快,它可以在 15-20 毫秒内执行 setTimeout(function() {}, 0),而旧的 Javascript 引擎需要数百毫秒才能执行该函数。尽管 setTimeout 使用毫秒,但目前没有任何 javascript 虚拟机可以以这种精度执行代码。

于 2008-10-12T20:50:29.910 回答
0

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

然而,重要的是 setTimeout/setInterval 在排队时执行的顺序——而且它总是能完美运行。

于 2008-10-12T21:09:09.700 回答
0

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

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

于 2008-10-12T22:13:09.430 回答
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

节点.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')
于 2017-09-29T08:01:50.563 回答
-1

您可以考虑使用html5 webaudio 时钟,它使用系统时间来获得更好的准确性

于 2015-05-31T11:19:09.310 回答