如何使`setInterval`更加同步,或者如何使用`setTimeout`替代?

101

我正在开发一个音乐程序,需要多个JavaScript元素与另一个元素保持同步。我一直在使用 setInterval,刚开始效果非常好。但是随着时间的推移,这些元素逐渐失去同步,在音乐程序中这是不好的。

我在网上看到setTimeout更准确,并且你可以以某种方式设置setTimeout循环。然而,我没有找到一个通用版本来说明如何实现这一点。

基本上,我有一些函数,如下所示:

//drums
setInterval(function {
  //code for the drums playing goes here
}, 8000);

//chords
setInterval(function {
  //code for the chords playing goes here
}, 1000);

//bass
setInterval(function {
  //code for the bass playing goes here
}, 500);

它最初运作得非常好,但大约一分钟后,声音会变得明显不同步,据我所读,这是使用 setInterval 时会发生的情况。 我已经阅读到 setTimeout 可以更加准确地工作。

是否有人可以向我展示一个基本的例子,如何使用 setTimeout 无限循环某些内容? 或者,如果有一种方法可以通过 setInterval 甚至其他函数实现更同步的结果,请告诉我。


2
为什么不发布一些代码,展示您想要实现的内容,这样我们可以给您更好的答案。 - Andy
1
“我在网上看到setTimeout更准确。” 你在哪里看到的?请附上链接。我猜这可能是因为使用setTimeout可以计算延迟的实际时间并调整下一个超时的时间。 - Matt Burland
2
requestAnimationFrame 怎么样?每次运行 requestAnimationFrame 回调时,您只需引用音频所在的时间即可。 - Jasper
5
任何一种类型的计时器并不能完全保证精准。所设置的毫秒数只是最小等待时间,但函数仍然可能会被延迟调用。如果您想要协调多个间隔,建议将其合并为一个可控制的间隔。 - Jonathan Lonowski
1
我认为Web Audio API可以提供你所尝试做的最准确的结果。 - powerc9000
显示剩余4条评论
13个回答

197
你可以使用递归创建一个setTimeout循环:
function timeout() {
    setTimeout(function () {
        // Do Something Here
        // Then recall the parent function to
        // create a recursive loop.
        timeout();
    }, 1000);
}

setInterval()setTimeout()的问题在于,无法保证你的代码将在指定的时间内运行。通过使用setTimeout()并递归调用它,确保在代码的下一次迭代开始之前,所有先前在超时内进行的操作都已经完成。


1
这个方法和使用setInterval有什么区别? - Jasper
4
这种方法存在问题,如果函数执行时间超过1000ms,则会完全失败。而且不能保证每隔1000ms执行一次,只有setInterval才能保证。 - TJC
1
@TJC:这在很大程度上取决于您想要实现什么。也许在下一次迭代之前,完成上一个函数更为重要,或者也可能不是。 - Matt Burland
7
如果您之前的操作在setInterval()重新执行之前尚未完成,那么您的变量和/或数据可能会非常快地不同步。例如,如果我正在使用ajax获取数据,而服务器需要超过1秒钟才能响应,则使用setInterval(),我的先前数据在下一个操作继续之前就没有完成处理。使用这种方法,不能保证您的函数每秒都会启动。但是,可以保证您的以前的数据将会在下一个时间间隔开始之前完成处理。 - War10ck
1
@War10ck,鉴于这是一个基于音乐的环境,我认为它不会用于设置变量或需要有序调用的ajax请求。 - TJC
如果我想从超时返回数据怎么办?@War10ck - NerdioN

35

仅作为补充说明。如果你需要传递一个变量并对其进行迭代,可以像这样操作:

function start(counter){
  if(counter < 10){
    setTimeout(function(){
      counter++;
      console.log(counter);
      start(counter);
    }, 1000);
  }
}
start(0);

输出:

1
2
3
...
9
10
每秒钟一行。

13

考虑到时间不会非常准确,使用 setTimeout 更准确的一种方式是计算自上次迭代以来的延迟时间,然后适当调整下一次迭代。例如:

var myDelay = 1000;
var thisDelay = 1000;
var start = Date.now();

function startTimer() {    
    setTimeout(function() {
        // your code here...
        // calculate the actual number of ms since last time
        var actual = Date.now() - start;
        // subtract any extra ms from the delay for the next cycle
        thisDelay = myDelay - (actual - myDelay);
        start = Date.now();
        // start the timer again
        startTimer();
    }, thisDelay);
}

因此,第一次它将等待(至少)1000毫秒,当您的代码被执行时,可能会有点晚,比如1046毫秒,所以我们从下一个周期的延迟中减去46毫秒,下一个延迟只有954毫秒。这不会阻止定时器延迟触发(这是预期的),但可以帮助您防止延迟堆积。(注意:你可能想检查thisDelay < 0,这意味着延迟超过了两倍的目标延迟,你错过了一个周期-由你决定如何处理这种情况)。

当然,这可能无法帮助您保持多个定时器同步,如果是这样,您可能需要想办法用同一个定时器控制它们所有。

因此,看着您的代码,所有的延迟都是500的倍数,所以您可以这样做:

var myDelay = 500;
var thisDelay = 500;
var start = Date.now();
var beatCount = 0;

function startTimer() {    
    setTimeout(function() {
        beatCount++;
        // your code here...
        //code for the bass playing goes here  

        if (count%2 === 0) {
            //code for the chords playing goes here (every 1000 ms)
        }

        if (count%16) {
            //code for the drums playing goes here (every 8000 ms)
        }

        // calculate the actual number of ms since last time
        var actual = Date.now() - start;
        // subtract any extra ms from the delay for the next cycle
        thisDelay = myDelay - (actual - myDelay);
        start = Date.now();
        // start the timer again
        startTimer();
    }, thisDelay);
}

1
+1个整洁的方法。我从未考虑过偏移下一次运行的超时时间。考虑到JavaScript不是多线程且不能保证在一致的时间间隔内触发,这可能会让你尽可能接近精确测量。 - War10ck
太好了!我会尝试一下并告诉你结果。我的代码很庞大,所以可能需要一些时间。 - user3084366
考虑:让这段代码在固定的时间循环上触发(即使像上面那样进行了更正)实际上可能不是解决问题的正确思路。你应该将每个间隔长度设置为“下一次需要播放的时间 - 当前时间”。 - mwardm
你做出了比我刚写下的更好的解决方案。我喜欢你的变量名使用音乐语言,并且你试图管理积累的延迟,而我甚至不尝试。 - Miguel Valencia

12

处理音频定时的最佳方法是使用Web Audio Api,它有一个单独的时钟,无论主线程发生什么事情都很准确。这里有来自Chris Wilson的很好的解释、示例等:

http://www.html5rocks.com/en/tutorials/audio/scheduling/

在这个网站上寻找更多关于Web Audio API的内容,它被开发出来正是为了满足你的需求。


我真的希望更多的人能够为此点赞。使用setTimeout的答案从不足到过于复杂,使用本地函数似乎是一个更好的主意。如果API不能满足您的目的,那么我建议尝试找到一个可靠的第三方调度库。 - trey-jones

6
根据您的要求:

请给我展示一个使用 setTimeout 循环某些内容的基本示例

我们有以下示例可供参考:

setTimeout可以用于循环执行函数,例如:

var itr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var  interval = 1000; //one second
itr.forEach((itr, index) => {

  setTimeout(() => {
    console.log(itr)
  }, index * interval)
})


6
使用 setInterval() 函数。
setInterval(function(){
 alert("Hello"); 
}, 3000);

以上代码将每3秒执行一次alert("Hello");


1
我在工作中使用的方法是:在这种情况下忘记常见的循环,而是使用包含“setTimeOut”的“setInterval”组合。
    function iAsk(lvl){
        var i=0;
        var intr =setInterval(function(){ // start the loop 
            i++; // increment it
            if(i>lvl){ // check if the end round reached.
                clearInterval(intr);
                return;
            }
            setTimeout(function(){
                $(".imag").prop("src",pPng); // do first bla bla bla after 50 millisecond
            },50);
            setTimeout(function(){
                 // do another bla bla bla after 100 millisecond.
                seq[i-1]=(Math.ceil(Math.random()*4)).toString();
                $("#hh").after('<br>'+i + ' : rand= '+(Math.ceil(Math.random()*4)).toString()+' > '+seq[i-1]);
                $("#d"+seq[i-1]).prop("src",pGif);
                var d =document.getElementById('aud');
                d.play();                   
            },100);
            setTimeout(function(){
                // keep adding bla bla bla till you done :)
                $("#d"+seq[i-1]).prop("src",pPng);
            },900);
        },1000); // loop waiting time must be >= 900 (biggest timeOut for inside actions)
    }

提示:了解(setTimeOut)的真实行为:它们都会在同一时间开始计时,“三个bla bla bla将同时开始倒计时”,因此需要设置不同的超时时间来安排执行。

提示2:这是定时循环的示例,但对于反应循环,您可以使用事件、promise、async await等。


1
setTimeout循环问题及解决方案。

// it will print 5 times 5.
for(var i=0;i<5;i++){
setTimeout(()=> 
console.log(i), 
2000)
}               // 5 5 5 5 5

// improved using let
for(let i=0;i<5;i++){
setTimeout(()=> 
console.log('improved using let: '+i), 
2000)
}

// improved using closure
for(var i=0;i<5;i++){
((x)=>{
setTimeout(()=> 
console.log('improved using closure: '+x), 
2000)
})(i);
} 


1
你有任何想法为什么在var和let之间表现不同吗? - Jay
1
@Jay...它们之间的区别在于var是函数作用域,而let是块级作用域。如果需要更多澄清,请参阅https://medium.com/@josephcardillo/the-difference-between-var-let-and-const-in-javascript-part-2-60fa568d0a0 - akhtarvahid

1

正如其他人指出的那样,Web Audio API 有更好的计时器。

但是一般来说,如果这些事件发生得很一致,为什么不将它们都放在同一个计时器上呢?我在思考一个 步进序列器 的工作原理。

实际上,它可能看起来像这样:

var timer = 0;
var limit = 8000; // 8000 will be the point at which the loop repeats

var drumInterval = 8000;
var chordInterval = 1000;
var bassInterval = 500;

setInterval(function {
    timer += 500;

    if (timer == drumInterval) {
        // Do drum stuff
    }

    if (timer == chordInterval) {
        // Do chord stuff
    }

    if (timer == bassInterval) {
        // Do bass stuff
    }

    // Reset timer once it reaches limit
    if (timer == limit) {
        timer = 0;
    }

}, 500); // Set the timer to the smallest common denominator

0
function timerCycle() {
  if (stoptime == false) {
    sec = parseInt(sec);
    min = parseInt(min);
    hr = parseInt(hr);
    sec = sec + 1;
    if (sec == 60) {
      min = min + 1;
      sec = 0;
    }

    if (min == 60) {
      hr = hr + 1;
      min = 0;
      sec = 0;
    }

    if (sec < 10 || sec == 0) {
      sec = "0" + sec;
    }
    if (min < 10 || min == 0) {
      min = "0" + min;
    }
    if (hr < 10 || hr == 0) {
      hr = "0" + hr;
    }

    timer.innerHTML = hr + " : " + min + " : " + sec;

    setTimeout(timerCycle, 1000);
  }
}

function startTimer() {
  if (stoptime == true) {
    stoptime = false;
    timerCycle();
  }
}

function stopTimer() {
  if (stoptime == false) {
    stoptime = true;
  }
}
function resetTimer() {
  hr = 0;
  min = 0;
  sec = 0;
  stopTimer();
  timer.innerHTML = "00:00:00";
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接