【问题标题】:Is there a more accurate way to create a Javascript timer than setTimeout?有没有比 setTimeout 更准确的方法来创建 Javascript 计时器?
【发布时间】:2010-09-16 19:09:06
【问题描述】:

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

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

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

【问题讨论】:

    标签: javascript timer


    【解决方案1】:

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

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

    【讨论】:

      【解决方案2】:

      这是我使用的。由于它是 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')
      

      【讨论】:

        【解决方案3】:

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

        【讨论】:

          【解决方案4】:

          这是我为我的一个音乐项目制作的计时器。在所有设备上都准确的计时器。

          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() 之前添加了以下内容:var startTime = new Date().getTime(); 并添加: var now = new Date().getTime(); var ms = (now - startTime) + 'ms'; 在回调中,结果是:这个计时器会漂移。从开始的毫秒数:100116ms
          【解决方案5】:

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

          【讨论】:

            【解决方案6】:

            这是一个演示 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()。我会相应地进行编辑。
            【解决方案7】:

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

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

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

            不幸的是,它目前仅在 IE9+ 中受支持,因为有一些来自其他浏览器供应商的 push back

            如果您想使用它并希望其他浏览器在某个时候实现它,可以使用good polyfill

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

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

            【讨论】:

              【解决方案8】:

              .

              参考; 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 变量?
              • @AntonRudeshko 嗯……好问题。我的代码改编自上面的链接。 I think it's just a useless leftover.
              • 那篇文章末尾的doTimer 函数正是我所需要的。非常准确。
              【解决方案9】:

              您需要在目标时间“爬上”。一些试验和错误是必要的,但本质上是。

              设置超时以在所需时间前大约 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 循环仍然会被其他进程打断,所以它仍然不完美。

              【讨论】:

              • 这不是一个糟糕的主意,假设您需要准确地打出挂钟时间。但是,如果您只是在一个一致的间隔之后,您可以通过简单地将超时值动态调整为略低于间隔时间减去处理所花费的时间来获得类似的结果。即使对于提醒或滴答时钟之类的东西,在大多数情况下,15 毫秒的准确度也可能足够好。
              • 我唯一需要这种准确性的时候是尝试实现音乐节拍。但是 Javascript 计时器并不真正适合这项工作。即使采用这种设计,滴答声也很不稳定,并不适合节拍器。
              【解决方案10】:

              shog9 的回答与我所说的差不多,尽管我会添加以下关于 UI 动画/事件的内容:

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

              【讨论】:

                【解决方案11】:

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

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

                【讨论】:

                • 为了清楚起见,Chrome 实际上支持 0ms 超时(而是在最直接的时间点执行它们)。
                【解决方案12】:

                有没有什么技巧可以做 确保setTimeout() 执行 准确地(不诉诸于 外部 API)或者这是一个失败的原因?

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

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

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

                【讨论】:

                • 从工人那里调用 setTimeout() 怎么样。不会减少渲染造成的阻塞吗?
                • 无法保证这一点,@hidarikani。 FWIW,这在本机代码中可能同样是一个问题,而在浏览器中,您没有能力要求高分辨率计时器和实时延迟。不是你想要的;这是浪费 CPU 的秘诀,您应该假设至少在某些情况下,浏览器和操作系统会尽其所能来延长电池寿命并为此目标牺牲计时器的准确性。
                • 我认为这个答案现在实际上是错误的,因为我们现在得到了requestAnimationFrame...另请参阅Christhis answer。这不是一个简单的解决方案,但它满足了 OP 的无外部 API 标准。
                • requestAnimationFrame 提供了更高的精度,但不一定是更好的精度@StijndeWitt - 你仍然受制于浏览器,这可能会被其他处理甚至其他任何事情停止在回调中实现时所做的选择。 Chris 的技术很好,但是如果您希望确保逻辑尽可能接近设定的时间执行,您仍然需要根据检索到的时间值来决定到期时间; requestAnimationFrame 提供的关于何时调用的保证甚至比 setTimeout 提供的还少。另请参阅我之前对 hidarikani 的说明。
                • "requestAnimationFrame 提供的保证更少" 这是一个很好的观点!谢谢
                【解决方案13】:

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

                然而,重要的是 setTimeout/setInterval 在排队时执行的顺序 - 并且始终完美运行。

                【讨论】:

                • 虽然我从不真正需要高精度计时,但当我为与 UI 相关的某些东西设置的延迟稍微偏离时,它可能会很麻烦。它不会破坏任何东西,但可以注意到它,因为它有点不一致。
                • Shog9 对我用简单语句包装的内容提供了完整的见解。是的,不能确保时间的不是“浏览器”或“javascript实现”,而是操作系统、人、世界等。因此,与其依赖“发生”,不如依赖“时间”。
                【解决方案14】:

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

                【讨论】:

                • 我听说 Chrome 是第一个如此准确的浏览器,但我仍然无法测试。
                • 你有点跑题了:15ms 是 Windows 上的标准计时器粒度,因此也是大多数浏览器 running 在 Windows 上的计时器粒度。 Chrome 显然正在使用更准确的硬件计时器来提供更好的粒度。 JS 执行速度是一个单独的问题。
                • Chrome 有这方面的错误 - 增加 setTimeout 分辨率会破坏某些假定为 15 毫秒分辨率的网站,并会破坏 Windows 的电池寿命(Windows 架构显然与体面的分辨率计时器作斗争)
                【解决方案15】:

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

                【讨论】:

                  【解决方案16】:

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

                  【讨论】:

                  • 我有一台双核 2.8 Ghz 机器,但计时器仍然非常不准确。根据我的经验,CPU 速度并不重要。
                  猜你喜欢
                  • 2011-11-19
                  • 2016-06-19
                  • 1970-01-01
                  • 2012-01-20
                  • 1970-01-01
                  • 1970-01-01
                  • 2013-07-21
                  • 1970-01-01
                  • 2018-06-29
                  相关资源
                  最近更新 更多