Як змусити `setInterval` поводитися більше синхронізовано, або як замість цього використовувати` setTimeout`?


91

Я працюю над музичною програмою, яка вимагає, щоб кілька елементів 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
Чому б вам не розмістити якийсь код, який показує нам, чого ви хочете досягти, і ми можемо дати вам кращі відповіді.
Енді

1
Я читав в Інтернеті, що setTimeout є більш точним : Де ви це читали? Включіть посилання. Я припускаю, що це, мабуть, випадок, коли setTimeoutви можете підрахувати, наскільки тривала затримка, щоб налаштувати час для наступного тайм-ауту.
Matt Burland

2
Що про requestAnimationFrame? Вам просто потрібно вказати час, який звучить при кожному requestAnimationFrameзапуску зворотного дзвінка.
Джаспер

5
Жоден тип таймера насправді не гарантується бути точним. Дані мілісекунди - це лише мінімальний час очікування, але функцію все одно можна викликати пізніше. Якщо ви намагаєтеся скоординувати кілька інтервалів, спробуйте замість цього консолідуватися до одного, контролюючи інтервал.
Джонатан Лоновський 03.03.14

1
Якщо ви дійсно хочете синхронізувати музику з чимось на екрані, під час оновлення DOM вам потрібно вказати часовий прогрес звуку. Інакше речі більшу частину часу будуть виходити з синхронізації.
Джаспер

Відповіді:


181

Ви можете створити 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?
Джаспер

4
проблема з цим підходом полягає в тому, що якщо функція займає більше 1000 мс, вона все йде вгору. це не гарантується для виконання кожні 1000 мс. setInterval є.
TJC 03.03.14

@TJC: Це дуже залежить від того, чого саме ти хочеш досягти. Можливо, важливішим є те, що попередня функція завершиться до наступної ітерації, а може, ні.
Matt Burland

4
@TJC Правильно, але якщо ваші попередні операції не будуть завершені до setInterval()повторного виконання, ваші змінні та / або дані можуть вийти з синхронізації дуже швидко. Наприклад, якщо я використовую ajaxing для даних, а сервер займає більше 1 секунди, щоб відповісти, використання setInterval()моїх попередніх даних не закінчило б обробку до моєї наступної операції. При такому підході не гарантується, що ваша функція почнеться щосекунди. Однак гарантовано, що ваші попередні дані закінчаться з обробкою до початку наступного інтервалу.
War10ck 03.03.14

1
@ War10ck, зважаючи на музичне середовище, я припустив, що воно не буде використовуватися для встановлення змінних чи викликів ajax там, де важливий порядок.
TJC 03.03.14

30

Тільки для доповнення. Якщо вам потрібно передати змінну та повторити її, ви можете зробити так:

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

Вихід:

1
2
3
...
9
10

Один рядок в секунду.


12

З огляду на те, що жоден час не буде дуже точним, одним із способів 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 03.03.14

Це чудово! Я спробую це і повідомлю вам, як це відбувається. Мій код масивний, тому, можливо, це займе деякий час
user3084366 03.03.14

Подумайте: наявність цього кодового тригера у фіксованому (навіть якщо він виправлений, як зазначено вище) часовому циклі може насправді бути неправильним способом думати про проблему. Можливо, вам слід фактично встановити для довжини кожного інтервалу значення "наступний раз-щось-це-має-бути-відтворено мінус поточний час".
mwardm

Ви зробили кращу версію рішення, яке я щойно записав. Мені подобається, що ваші імена змінних використовують мову з музики, і що ви намагаєтесь керувати затримками, що накопичуються, чого я навіть не намагаюся.
Мігель Валенсія,

8

Найкращий спосіб справитись із синхронізацією звуку - це Web Audio Api, він має окремий годинник, який є точним, незалежно від того, що відбувається в основному потоці. Тут є чудове пояснення, приклади тощо від Кріса Вільсона:

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

Подивіться на цьому веб-сайті, щоб дізнатися більше про веб-аудіо API, він був розроблений для того, щоб робити саме те, що вам потрібно.


Я дуже хочу, щоб більше людей підтримало це. Відповіді, що використовуються, setTimeoutваріюються від недостатньої до жахливо надмірно складної. Використання власної функції здається набагато кращою ідеєю. Якщо API не відповідає вашим цілям, я рекомендую спробувати знайти надійну сторонню бібліотеку планування.
три вечора


3

Відповідно до вашої вимоги

просто покажіть мені базовий приклад використання 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)
})


1

Я використовую такий спосіб у робочому житті: "Забути загальні цикли" в цьому випадку і використовую цю комбінацію "setInterval", включає "setTimeOut" s:

    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: Зрозумійте, що справжня поведінка (setTimeOut): всі вони почнуться одночасно "три бла бла почнуть відлік в ту ж мить", тому зробіть інший тайм-аут, щоб домовитись про виконання.

PS 2: приклад для циклу синхронізації, але для циклу реакції ви можете використовувати події, обіцяйте 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?
Джей,

1
@Jay ... Різниця між ними полягає в тому, що var - це функція, а не блок. для отримання детальніших роз'яснень ви можете пройти через medium.com/@josephcardillo/…
Вахід Ахтар

1

Як зазначав хтось інший, 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 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
Пояснення вашого рішення були б дуже вдячні!
тон

-2

Я думаю, що краще закінчити час очікування в кінці функції.

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

Ці три функції необхідні, оскільки вари у другій функції будуть кожного разу перезаписуватися зі значенням за замовчуванням. Коли покажчик програми досягає setTimeout, один крок вже обчислюється. Тоді просто екрану потрібно трохи часу.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.