香草JavaScript中的精确计时器

香草JavaScript中的精确计时器,javascript,datetime,Javascript,Datetime,我经常制作计时器/秒表,但总是遇到问题 使用setInterval()时,它很少是精确的。我正在使用Safari 8,20秒后,它最多可以关闭+-8秒。我尝试每10毫秒计时一次,所以requestFrameAnimation不会剪切它,因为我没有设置动画 我提出了以下使用Date对象的方法 var runs = 0, speed = 10, timeout = speed, time = Math.floor(new Date().getTime() / speed);

我经常制作计时器/秒表,但总是遇到问题

使用
setInterval()时,它很少是精确的。我正在使用Safari 8,20秒后,它最多可以关闭+-8秒。我尝试每10毫秒计时一次,所以
requestFrameAnimation
不会剪切它,因为我没有设置动画

我提出了以下使用
Date
对象的方法

var runs = 0,
    speed = 10,
    timeout = speed,
    time = Math.floor(new Date().getTime() / speed);

function timer () {

    runs += 1;

    console.log(runs, new Date().getTime() / speed);

    if (Math.floor(new Date().getTime() / speed) > time + 1) {
        timeout = speed - (Math.floor(new Date().getTime() / speed) - time);
    } else if (Math.floor(new Date().getTime() / speed) < time + 1) {
        timeout = speed + (time - Math.floor(new Date().getTime() / speed));
    } else {
        timeout = speed;
    }

    time = Math.floor(new Date().getTime() / speed);

    setTimeout(function () {
        timer(); //Repeat
    }, timeout);
}

timer();//Starts timer
var运行=0,
速度=10,
超时=速度,
time=Math.floor(new Date().getTime()/speed);
函数计时器(){
运行次数+=1;
log(runs,new Date().getTime()/speed);
if(Math.floor(new Date().getTime()/speed)>time+1){
超时=速度-(数学地板(新日期().getTime()/speed)-时间);
}else if(Math.floor(new Date().getTime()/speed)
除此之外,在100毫秒内仍可以运行3次(
.6
.64
.68
),约为速度的三分之一

我已经看到了许多解决方案,说明了如何使用其他语言(如
Node.js、Java、C#
)甚至JavaScript库来实现这一点,但我似乎无法解决这个基本问题


我有什么明显遗漏的吗?最好的方法是什么?

如果您尝试每10秒运行一次,您的问题是初始速度设置为10吗?1000毫秒=1秒,因此10/1000=1/100秒。此外,getTime函数还返回一个Unix时间戳。除非您记录上一个时间戳,减去当前时间戳并调整执行时间中任何偏移的速度,否则除以速度没有多大意义。

如果您尝试每10秒运行一次,您的问题是初始速度设置为10吗?1000毫秒=1秒,因此10/1000=1/100秒。此外,getTime函数还返回一个Unix时间戳。除非您记录上一个时间戳,减去当前时间戳并调整执行时间中任何偏移量的速度,否则除以速度没有多大意义。

由于两个原因,您想要做的技术上是不可能的:

  • 运行代码的机器的速度

  • JavaScript的同步特性

  • 解释一下,如果一台计算机不能在10毫秒或100毫秒内执行给定的指令,它就不会执行。您所能做的最好的事情就是尝试调整延迟,但即使如此,机器正在进行的任何其他处理以及浏览器正在执行的任何其他JavaScript都将阻止“线程”的执行(我松散地使用这个术语)

    我只是写了一些代码来测试这一点,因为我很好奇,平均来说,我能写的最有效的函数是在给定10秒超时的情况下,超出准确计时的67%以内


    我知道这是个坏消息,但这是事实。你想做的事无法完成。

    你想做的事在技术上是不可能的,原因有两个:

  • 运行代码的机器的速度

  • JavaScript的同步特性

  • 解释一下,如果一台计算机不能在10毫秒或100毫秒内执行给定的指令,它就不会执行。您所能做的最好的事情就是尝试调整延迟,但即使如此,机器正在进行的任何其他处理以及浏览器正在执行的任何其他JavaScript都将阻止“线程”的执行(我松散地使用这个术语)

    我只是写了一些代码来测试这一点,因为我很好奇,平均来说,我能写的最有效的函数是在给定10秒超时的情况下,超出准确计时的67%以内


    我知道这是个坏消息,但这是事实。你想做的事是无法完成的。

    有两种方法可以得到你想要的

  • 使用后台进程(web worker)
  • 在开始时根据时间戳跟踪时间,并显示现在和那时之间的差异,而不是每间隔帧递增

  • 网络工作者版本 如果您可以支持,那么您可以使用它们来运行一个专用的后台进程,该进程支持您想要执行的时间帧推送类型(在一个间隔内逐帧递增一个计时器帧,无时间戳差异,并保持其准确性)

    以下是此网页上的一个示例:


    非网络工作者版本 以及非网络工作者版本:

    <p id='timer'>0.00</p>
    <p id='starter-container'>
        <button type='button' id='starter'>Start</button>
        <button type='button' id='starter-reset'>Reset</button>
    </p>
    <script>
    (function oab(){ // Keep it local.
    var runs = 0,
        max_runs = 10000,
        speed = 10,
        timeout = speed,
        start_time = 0,
        time = 0,
        num_seconds = (30) * 1000,
        mark_every = 100,
        mark_next = time * speed,
        timer_el = document.getElementById('timer'),
        starter = document.getElementById('starter'),
        reset = document.getElementById('starter-reset');
    
    starter.addEventListener('click', function cl(){
        reset_timer();
        init_timer();
        do_timer();
        this.disabled = true;
    });
    
    reset.addEventListener('click', function cl(){
        runs = max_runs++;
    });
    
    function init_timer() {
        start_time = new Date().getTime();
        time = Math.floor(start_time / speed);
    }
    
    function reset_timer() {
        runs = 0;
        starter.disabled = false;
        timer_el.innerText = '0.00';
    }
    
    function do_timer(){
        init_timer();
    
        (function timer () {
            var c_time = new Date().getTime(),
                time_diff = c_time - start_time,
                c_secs = 0;
    
            runs += 1;
    
            c_secs = (Math.round(time_diff / 10, 3) / 100).toString();
    
            if (c_secs.indexOf('.') === -1) {
                c_secs += '.00';
            } else if (c_secs.split('.').pop().toString().length === 1 ) {
                c_secs += '0';
            }
    
            timer_el.innerText = c_secs;
    
            if (c_time >= mark_next) {
                console.log(
                    'mark_next: ' + mark_next,
                    'mark time: ' + c_time, 
                    '(' + (Math.floor(c_time * .01) * 100).toString().substring(10) + ')', 
                    'precision: ' + (mark_next - c_time) + ')'
                );
    
                mark_next = Math.floor((c_time + mark_every) * .01) * 100;
            }
    
            if (Math.floor(c_time / speed) > time + 1) {
                timeout = speed - ((c_time / speed) - time);
            } else if (Math.floor(c_time / speed) < time + 1) {
                timeout = speed + (time - Math.floor(c_time / speed));
            } else {
                timeout = speed;
            }
    
            time = Math.floor(new Date().getTime() / speed);
    
            if (runs >= max_runs || time_diff > num_seconds) {
                reset_timer();
    
                return;
            }
    
            setTimeout(timer, timeout);
        })();
    }
    })();
    </script>
    

    0.00

    开始 重置

    (函数oab(){//将其保持为本地。 var运行=0, 最大运行次数=10000次, 速度=10, 超时=速度, 开始时间=0, 时间=0, 秒数=(30)*1000, 马克每100分, mark_next=时间*速度, timer\u el=document.getElementById('timer'), starter=document.getElementById('starter'), reset=document.getElementById('starter-reset'); starter.addEventListener('click',函数cl(){ 重置计时器(); 初始化计时器(); do_timer(); this.disabled=true; }); reset.addEventListener('click',函数cl(){ 运行次数=最大运行次数++; }); 函数init_timer(){ 开始时间=新日期().getTime(); 时间=数学地板(开始时间/速度); } 功能复位定时器(){ 运行次数=0; starter.disabled=false; timer_el.innerText='0.00'; } 函数do_timer(){ 初始化计时器(); (功能计时器(){ var c_time=new Date().getTime(), 时间差=c时间-开始时间, c_secs=0; 运行次数+=1; c_secs=(Math.round(time_diff/10,3)/100).toString(); 如果(c_secs.indexOf('.')=-1){ c_secs+='0.00'; }否则如果(c_secs.split('.').pop().toString().length==1
    var timerStart = true;
    
    function myTimer(d0){
       // get current time
       var d=(new Date()).valueOf();
       // calculate time difference between now and initial time
       var diff = d-d0;
       // calculate number of minutes
       var minutes = Math.floor(diff/1000/60);
       // calculate number of seconds
       var seconds = Math.floor(diff/1000)-minutes*60;
       var myVar = null;
       // if number of minutes less than 10, add a leading "0"
       minutes = minutes.toString();
       if (minutes.length == 1){
          minutes = "0"+minutes;
       }
       // if number of seconds less than 10, add a leading "0"
       seconds = seconds.toString();
       if (seconds.length == 1){
          seconds = "0"+seconds;
       }
    
       // return output to Web Worker
       postMessage(minutes+":"+seconds);
    }
    
    if (timerStart){
       // get current time
       var d0=(new Date()).valueOf();
       // repeat myTimer(d0) every 100 ms
       myVar=setInterval(function(){myTimer(d0)},100);
       // timer should not start anymore since it has been started
       timerStart = false;
    }
    
    <p id='timer'>0.00</p>
    <p id='starter-container'>
        <button type='button' id='starter'>Start</button>
        <button type='button' id='starter-reset'>Reset</button>
    </p>
    <script>
    (function oab(){ // Keep it local.
    var runs = 0,
        max_runs = 10000,
        speed = 10,
        timeout = speed,
        start_time = 0,
        time = 0,
        num_seconds = (30) * 1000,
        mark_every = 100,
        mark_next = time * speed,
        timer_el = document.getElementById('timer'),
        starter = document.getElementById('starter'),
        reset = document.getElementById('starter-reset');
    
    starter.addEventListener('click', function cl(){
        reset_timer();
        init_timer();
        do_timer();
        this.disabled = true;
    });
    
    reset.addEventListener('click', function cl(){
        runs = max_runs++;
    });
    
    function init_timer() {
        start_time = new Date().getTime();
        time = Math.floor(start_time / speed);
    }
    
    function reset_timer() {
        runs = 0;
        starter.disabled = false;
        timer_el.innerText = '0.00';
    }
    
    function do_timer(){
        init_timer();
    
        (function timer () {
            var c_time = new Date().getTime(),
                time_diff = c_time - start_time,
                c_secs = 0;
    
            runs += 1;
    
            c_secs = (Math.round(time_diff / 10, 3) / 100).toString();
    
            if (c_secs.indexOf('.') === -1) {
                c_secs += '.00';
            } else if (c_secs.split('.').pop().toString().length === 1 ) {
                c_secs += '0';
            }
    
            timer_el.innerText = c_secs;
    
            if (c_time >= mark_next) {
                console.log(
                    'mark_next: ' + mark_next,
                    'mark time: ' + c_time, 
                    '(' + (Math.floor(c_time * .01) * 100).toString().substring(10) + ')', 
                    'precision: ' + (mark_next - c_time) + ')'
                );
    
                mark_next = Math.floor((c_time + mark_every) * .01) * 100;
            }
    
            if (Math.floor(c_time / speed) > time + 1) {
                timeout = speed - ((c_time / speed) - time);
            } else if (Math.floor(c_time / speed) < time + 1) {
                timeout = speed + (time - Math.floor(c_time / speed));
            } else {
                timeout = speed;
            }
    
            time = Math.floor(new Date().getTime() / speed);
    
            if (runs >= max_runs || time_diff > num_seconds) {
                reset_timer();
    
                return;
            }
    
            setTimeout(timer, timeout);
        })();
    }
    })();
    </script>