Jak sprawić, by „setInterval” zachowywał się bardziej synchronicznie, lub jak zamiast tego używać „setTimeout”?


91

Pracuję nad programem muzycznym, który wymaga synchronizacji wielu elementów JavaScript z innymi. Używałem setInterval, co początkowo działa bardzo dobrze. Jednak z biegiem czasu elementy stopniowo tracą synchronizację, co jest złe w programie muzycznym.

Czytałem w Internecie, że setTimeoutjest dokładniejszy i setTimeoutjakoś możesz mieć pętle. Jednak nie znalazłem ogólnej wersji, która ilustruje, jak to jest możliwe.

Zasadniczo mam kilka funkcji, takich jak:

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

Początkowo działa bardzo dobrze, ale w ciągu około minuty dźwięki stają się zauważalnie niezsynchronizowane, jak to czytałem setInterval. Czytałem, że setTimeoutmoże być bardziej konsekwentnie dokładne.

Czy ktoś mógłby mi po prostu pokazać podstawowy przykład użycia setTimeoutdo zapętlenia czegoś w nieskończoność? Ewentualnie, jeśli istnieje sposób na osiągnięcie bardziej synchronicznych wyników z setIntervalinną funkcją lub nawet inną, proszę o informację.


2
Dlaczego nie prześlesz nam kodu pokazującego nam, co chcesz osiągnąć, a my udzielimy Ci lepszych odpowiedzi.
Andy

1
Czytałem w internecie, że setTimeout jest dokładniejszy : Gdzie to przeczytałeś? Dołącz link. Zakładam, że jest to prawdopodobnie przypadek, w setTimeoutktórym możesz obliczyć, jak długo naprawdę było opóźnienie, dostosuj czas do następnego limitu czasu.
Matt Burland,

2
O co chodzi requestAnimationFrame? Wystarczy, że odniesiesz się do czasu, w którym dźwięk jest za każdym razem, gdy requestAnimationFrameuruchomione jest wywołanie zwrotne.
Jasper

5
Żaden typ timera nie gwarantuje dokładności. Podane milisekundy to tylko minimalny czas oczekiwania, ale funkcję można nadal wywołać później. Jeśli próbujesz skoordynować wiele interwałów, spróbuj zamiast tego skonsolidować do jednego, kontrolując interwał.
Jonathan Lonowski

1
Jeśli naprawdę chcesz zsynchronizować muzykę z czymś na ekranie, musisz odnosić się do postępu czasu w dźwięku podczas aktualizacji DOM. W przeciwnym razie przez większość czasu rzeczy nie będą zsynchronizowane.
Jasper

Odpowiedzi:


181

Możesz utworzyć setTimeoutpętlę za pomocą rekurencji:

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

Problem z setInterval()i setTimeout()polega na tym, że nie ma gwarancji, że kod zostanie uruchomiony w określonym czasie. Używając setTimeout()i wywołując go rekurencyjnie, masz pewność, że wszystkie poprzednie operacje w ramach limitu czasu zostaną zakończone przed rozpoczęciem następnej iteracji kodu.


1
Jaka jest różnica między tą metodą a używaniem setInterval?
Jasper

4
Problem z tym podejściem polega na tym, że jeśli wykonanie funkcji trwa dłużej niż 1000 ms, wszystko idzie do góry nogami. to nie jest gwarantowane co 1000 ms. setInterval to.
TJC

@TJC: To bardzo zależy od tego, co dokładnie chcesz osiągnąć. Może być ważniejsze, aby poprzednia funkcja zakończyła się przed następną iteracją, a może nie.
Matt Burland,

4
@TJC Prawidłowo, ale jeśli poprzednie operacje nie zostaną zakończone przed setInterval()ponownym wykonaniem, Twoje zmienne i / lub dane mogą bardzo szybko stracić synchronizację. Jeśli na przykład AJAX wyszukuje dane, a odpowiedź serwera trwa dłużej niż 1 sekundę, setInterval()moje poprzednie dane nie zostałyby zakończone przed kontynuowaniem mojej następnej operacji. Przy takim podejściu nie ma gwarancji, że Twoja funkcja będzie się uruchamiać co sekundę. Jednak gwarantuje się, że poprzednie dane zostaną przetworzone przed rozpoczęciem kolejnego interwału.
War10ck

1
@ War10ck, biorąc pod uwagę środowisko muzyczne, założyłem, że nie będzie ono używane do ustawiania zmiennych lub wywołań Ajax, w których liczy się kolejność.
TJC

30

Tylko do uzupełnienia. Jeśli potrzebujesz przekazać zmienną i ją iterować, możesz zrobić tak:

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

Wynik:

1
2
3
...
9
10

Jedna linia na sekundę.


12

Biorąc pod uwagę, że żaden z tych czasów nie będzie bardzo dokładny, jednym ze sposobów, aby setTimeoutbyć nieco dokładniejszym, jest obliczenie czasu opóźnienia od ostatniej iteracji, a następnie odpowiednie dostosowanie następnej iteracji. Na przykład:

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

Więc za pierwszym razem, gdy będzie czekał (przynajmniej) 1000 ms, kiedy twój kod zostanie wykonany, może być trochę spóźniony, powiedzmy 1046 ms, więc odejmujemy 46 ms od naszego opóźnienia na następny cykl, a następne opóźnienie będzie tylko 954 ms. Nie zatrzyma to opóźnionego uruchamiania timera (należy się tego spodziewać), ale pomoże ci zapobiec narastaniu opóźnień. (Uwaga: możesz chcieć sprawdzić, thisDelay < 0co oznacza, że ​​opóźnienie było ponad dwukrotnie większe od docelowego opóźnienia i przegapiłeś cykl - zależy od Ciebie, jak chcesz sobie z tym poradzić).

Oczywiście prawdopodobnie nie pomoże ci to zsynchronizować kilku timerów, w takim przypadku możesz chcieć dowiedzieć się, jak kontrolować je wszystkie za pomocą tego samego timera.

Patrząc na kod, wszystkie opóźnienia są wielokrotnością 500, więc możesz zrobić coś takiego:

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 Schludne podejście. Nigdy nie myślałem o wyrównaniu limitu czasu następnego biegu. To prawdopodobnie zbliżyłoby Cię do dokładnego pomiaru, jaki możesz uzyskać, biorąc pod uwagę, że JavaScript nie jest wielowątkowy i nie gwarantuje, że będzie uruchamiany w stałych odstępach czasu.
War10ck

To jest świetne! Spróbuję tego i dam ci znać, jak to idzie. Mój kod jest ogromny, więc prawdopodobnie zajmie to trochę czasu
user3084366

Rozważ: Posiadanie tego kodu wyzwalającego w ustalonej (nawet poprawionej, jak powyżej) pętli czasowej może być w rzeczywistości niewłaściwym sposobem myślenia o problemie. Może się tak zdarzyć, że w rzeczywistości powinieneś ustawić długość każdego interwału na „następny-raz-coś-ma-być-zagrane minus bieżący-czas”.
mwardm

Stworzyłeś lepszą wersję rozwiązania, które właśnie zapisałem. Podoba mi się, że twoje nazwy zmiennych używają języka z muzyki i że próbujesz zarządzać tworzącymi się opóźnieniami, czego nawet nie próbuję.
Miguel Valencia

8

Najlepszym sposobem radzenia sobie z synchronizacją dźwięku jest interfejs Web Audio Api, który ma oddzielny zegar, który jest dokładny niezależnie od tego, co dzieje się w głównym wątku. Tutaj jest świetne wyjaśnienie, przykłady itp. Od Chrisa Wilsona:

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

Rozejrzyj się po tej witrynie, aby znaleźć więcej interfejsu API Web Audio, został on opracowany, aby robić dokładnie to, czego szukasz.


Naprawdę chciałbym, żeby więcej ludzi to zagłosowało. Odpowiedzi za pomocą setTimeoutzakresu od niewystarczającego do strasznie skomplikowanego. Używanie funkcji natywnej wydaje się znacznie lepszym pomysłem. Jeśli interfejs API nie służy Twojemu celowi, polecam znalezienie niezawodnej biblioteki planowania innej firmy.
trzy czwarte


3

Zgodnie z twoim wymaganiem

po prostu pokaż mi podstawowy przykład użycia setTimeout do zapętlenia czegoś

mamy następujący przykład, który może ci pomóc

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

W życiu zawodowym używam tego sposobu: „Zapomnij o zwykłych pętlach” w tym przypadku i używam tej kombinacji „setInterval” zawiera „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: Zrozum, że prawdziwe zachowanie (setTimeOut): wszystkie zaczną się w tym samym czasie „trzy bla bla bla zaczną odliczać w tym samym momencie”, więc ustaw inny czas na wykonanie.

PS 2: przykład pętli czasowej, ale w przypadku pętli reakcji można używać zdarzeń, obiecywać asynchroniczne oczekiwanie.


1

Problem z pętlą setTimeout z rozwiązaniem

// 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
Masz jakiś pomysł, dlaczego zachowuje się inaczej między var i let?
Jay

1
@Jay ... Różnica między nimi polega na tym, że var jest objęty zakresem funkcji, a let blokiem. więcej wyjaśnień można uzyskać
Vahid Akhtar

1

Jak ktoś zauważył, interfejs API Web Audio ma lepszy zegar.

Ale ogólnie, jeśli te zdarzenia mają miejsce konsekwentnie, co powiesz na ustawienie ich wszystkich na tym samym zegarze? Myślę o tym, jak działa sekwencer krokowy .

Praktycznie, czy mogłoby to wyglądać mniej więcej tak?

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
Bardzo mile widziane byłoby wyjaśnienie Twojego rozwiązania!
ton

-2

Myślę, że lepiej jest przekroczyć limit czasu na końcu funkcji.

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

Te trzy funkcje są niezbędne, ponieważ zmienne w drugiej funkcji będą za każdym razem nadpisywane wartością domyślną. Gdy wskaźnik programu osiągnie wartość setTimeout, jeden krok jest już obliczany. Wtedy tylko ekran potrzebuje trochę czasu.


-2

Użyj let zamiast var w kodzie:

for(let i=1;i<=5;i++){setTimeout(()=>{console.log(i)},1000);}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.