Come fare in modo che `setInterval` si comporti in modo più sincronizzato, o come usare invece` setTimeout`?


91

Sto lavorando a un programma musicale che richiede che più elementi JavaScript siano sincronizzati con un altro. Sto usando setInterval, che inizialmente funziona molto bene. Tuttavia, nel tempo gli elementi perdono gradualmente la sincronizzazione, il che è negativo in un programma musicale.

Ho letto online che setTimeoutè più accurato e puoi avere dei setTimeoutloop in qualche modo. Tuttavia, non ho trovato una versione generica che illustri come ciò sia possibile.

Fondamentalmente ho alcune funzioni come queste:

//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);

Funziona molto bene, inizialmente, ma nel corso di circa un minuto, i suoni diventano notevolmente fuori sincrono come ho letto setInterval. Ho letto che setTimeoutpuò essere sempre più accurato.

Qualcuno potrebbe mostrarmi un semplice esempio di utilizzo setTimeoutper ripetere qualcosa indefinitamente? In alternativa, se esiste un modo per ottenere risultati più sincroni con setIntervalo anche un'altra funzione, fammelo sapere.


2
Perché non pubblichi un codice che ci mostri cosa vuoi ottenere e possiamo darti risposte migliori.
Andy

1
Ho letto online che setTimeout è più preciso : dove l'hai letto? Includere un collegamento. Suppongo che probabilmente sia un caso in setTimeoutcui puoi calcolare per quanto tempo il ritardo è stato davvero un aggiustamento del tempo per il prossimo timeout.
Matt Burland

2
Di cosa requestAnimationFrame? Dovresti solo fare riferimento al tempo in cui l'audio è ogni volta che requestAnimationFrameviene eseguito il callback.
Jasper

5
Nessuno dei due tipi di timer è davvero garantito per essere preciso. I millisecondi forniti sono solo un tempo di attesa minimo, ma la funzione può ancora essere chiamata in seguito. Se stai cercando di coordinare più intervalli, prova invece a consolidare in uno, controllando l'intervallo.
Jonathan Lonowski

1
Se vuoi davvero sincronizzare la musica con qualcosa sullo schermo, devi fare riferimento al progresso del tempo attraverso l'audio quando aggiorni il DOM. Altrimenti le cose andranno fuori sincronia la maggior parte del tempo.
Jasper

Risposte:


181

Puoi creare un setTimeoutciclo usando la ricorsione:

function timeout() {
    setTimeout(function () {
        // Do Something Here
        // Then recall the parent function to
        // create a recursive loop.
        timeout();
    }, 1000);
}

Il problema con setInterval()e setTimeout()è che non vi è alcuna garanzia che il codice verrà eseguito nel tempo specificato. Usandolo setTimeout()e chiamandolo in modo ricorsivo, ti assicuri che tutte le operazioni precedenti all'interno del timeout siano complete prima che inizi l'iterazione successiva del codice.


1
Qual è la differenza tra questo metodo e l'utilizzo setInterval?
Jasper

4
il problema con questo approccio è che se la funzione impiega più di 1000 ms per essere completata, tutto va a gambe all'aria. non è garantito che venga eseguito ogni 1000 ms. setInterval è.
TJC

@TJC: Dipende molto esattamente da ciò che stai cercando di ottenere. Potrebbe essere più importante che la funzione precedente venga completata prima dell'iterazione successiva, o forse no.
Matt Burland

4
@TJC Corretto, ma se le operazioni precedenti non sono state completate prima della setInterval()riesecuzione, le variabili e / oi dati potrebbero perdere la sincronizzazione molto velocemente. Se ad esempio eseguo un ajax per i dati e il server impiega più di 1 secondo per rispondere, l'utilizzo setInterval()dei dati precedenti non avrebbe terminato l'elaborazione prima che l'operazione successiva continuasse. Con questo approccio, non è garantito che la funzione venga avviata ogni secondo. Tuttavia, è garantito che i tuoi dati precedenti avranno terminato l'elaborazione prima dell'inizio dell'intervallo successivo.
War10ck

1
@ War10ck, dato un ambiente basato sulla musica, pensavo che non sarebbe stato usato per impostare variabili o chiamate ajax dove l'ordine era importante.
TJC

30

Solo per completare. Se devi passare una variabile e iterarla, puoi fare proprio così:

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

Produzione:

1
2
3
...
9
10

Una riga al secondo.


12

Dato che nessuno dei due tempi sarà molto preciso, un modo da utilizzare setTimeoutper essere un po 'più accurati è calcolare quanto tempo è stato il ritardo dall'ultima iterazione e quindi regolare l'iterazione successiva come appropriato. Per esempio:

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

Quindi la prima volta aspetterà (almeno) 1000 ms, quando il tuo codice viene eseguito, potrebbe essere un po 'in ritardo, diciamo 1046 ms, quindi sottraiamo 46 ms dal nostro ritardo per il ciclo successivo e il ritardo successivo sarà solo 954 ms. Ciò non impedirà al timer di attivarsi in ritardo (è prevedibile), ma ti aiuterà a evitare che i ritardi si accumulino. (Nota: potresti voler verificare il motivo per thisDelay < 0cui il ritardo era più del doppio del tuo ritardo target e hai perso un ciclo - dipende da te come vuoi gestire quel caso).

Ovviamente, questo probabilmente non ti aiuterà a mantenere sincronizzati diversi timer, nel qual caso potresti voler capire come controllarli tutti con lo stesso timer.

Quindi, guardando il tuo codice, tutti i tuoi ritardi sono multipli di 500, quindi potresti fare qualcosa del genere:

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 Approccio accurato. Non ho mai pensato di compensare il periodo di timeout della prossima esecuzione. Questo probabilmente ti porterebbe il più vicino possibile a una misurazione esatta considerando che JavaScript non è multi-thread e non è garantito che si attivi in ​​un intervallo di tempo coerente.
War10ck

È fantastico! Lo proverò e ti farò sapere come va. Il mio codice è enorme quindi probabilmente ci vorrà un po 'di tempo
user3084366

Considera: avere questo codice trigger su un ciclo temporale fisso (anche se corretto, come sopra) potrebbe effettivamente essere il modo sbagliato di pensare al problema. Potrebbe essere che dovresti effettivamente impostare la lunghezza di ogni intervallo su "la prossima volta che qualcosa deve essere riprodotto meno l'ora corrente".
mwardm

Hai realizzato la versione migliore della soluzione che ho appena annotato. Mi piace che i nomi delle tue variabili usino il linguaggio della musica e che tu provi a gestire i ritardi che si accumulano, cosa che non provo nemmeno.
Miguel Valencia

8

Il modo migliore per gestire la temporizzazione dell'audio è con Web Audio Api, ha un clock separato che è accurato indipendentemente da ciò che sta accadendo nel thread principale. C'è una grande spiegazione, esempi, ecc da Chris Wilson qui:

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

Dai un'occhiata a questo sito per ulteriori API Web Audio, è stato sviluppato per fare esattamente quello che stai cercando.


Vorrei davvero che più persone lo votassero. Le risposte che utilizzano setTimeoutvanno da insufficiente a orribilmente complesso. L'utilizzo di una funzione nativa sembra un'idea molto migliore. Se l'API non serve al tuo scopo, ti consiglio di provare a trovare una libreria di pianificazione di terze parti affidabile.
tre del


3

Secondo il vostro requisito

mostrami un semplice esempio di utilizzo di setTimeout per eseguire il loop di qualcosa

abbiamo il seguente esempio che può aiutarti

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


1

Uso questo modo nella vita lavorativa: "Dimentica i loop comuni" in questo caso e uso questa combinazione di "setInterval" che include "setTimeOut":

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

PS: Capisci che il comportamento reale di (setTimeOut): inizieranno tutti nello stesso tempo "i tre bla bla bla inizieranno il conto alla rovescia nello stesso momento" quindi fai un timeout diverso per organizzare l'esecuzione.

PS 2: l'esempio per il loop di temporizzazione, ma per un loop di reazione puoi utilizzare eventi, prometti asincroni attesi


1

problema di ciclo setTimeout con soluzione

// 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
Qualche idea sul perché si comporta in modo diverso tra var e let?
Jay

1
@ Jay ... La differenza tra loro è che var è con ambito di funzione e let è con ambito di blocco. per maggiori chiarimenti puoi andare su medium.com/@josephcardillo/…
Vahid Akhtar

1

Come qualcun altro ha sottolineato, l'API Web Audio ha un timer migliore.

Ma in generale, se questi eventi si verificano in modo coerente, che ne dici di metterli tutti sullo stesso timer? Sto pensando a come funziona uno step sequencer .

In pratica, potrebbe sembrare qualcosa di simile?

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 appendTaskOnStack(task, ms, loop) {
    window.nextTaskAfter = (window.nextTaskAfter || 0) + ms;

    if (!loop) {
        setTimeout(function() {
            appendTaskOnStack(task, ms, true);
        }, window.nextTaskAfter);
    } 
    else {
        if (task) 
            task.apply(Array(arguments).slice(3,));
        window.nextTaskAfter = 0;
    }
}

for (var n=0; n < 10; n++) {
    appendTaskOnStack(function(){
        console.log(n)
    }, 100);
}


1
Una spiegazione della tua soluzione sarebbe molto apprezzata!
ton

-2

Penso che sia meglio il timeout alla fine della funzione.

function main(){
    var something; 
    make=function(walkNr){
         if(walkNr===0){
           // var something for this step      
           // do something
         }
         else if(walkNr===1){
           // var something for that step 
           // do something different
         }

         // ***
         // finally
         else if(walkNr===10){
           return something;
         }
         // show progress if you like
         setTimeout(funkion(){make(walkNr)},15,walkNr++);  
   }
return make(0);
}   

Queste tre funzioni sono necessarie perché le variabili nella seconda funzione verranno sovrascritte ogni volta con il valore predefinito. Quando il puntatore del programma raggiunge il setTimeout, un passo è già calcolato. Quindi solo lo schermo ha bisogno di un po 'di tempo.


-2

Usa let invece di var nel codice:

for(let i=1;i<=5;i++){setTimeout(()=>{console.log(i)},1000);}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.