JavaScript: цикличные таймеры с автокоррекцией

от автора

В посте в повествовательной и не очень манере рассказывается о различных реализациях «точных» таймеров на JS. Материал рассчитан на новичков… Добро пожаловать под кат.

Как заметили многие, на картинке к посту изображены часы из работы Дали «Время течет», выбор отнюдь не случаен и метафоричен по своей сути. Ибо, в рамках программирования на JS, время может течь не совсем так, как мы это предполагаем. JS однопоточен по своей сути, что порождает очередь выполнения функций, а очередь подразумевает непременный порядок следования. И если некоторые из этапов вычислений оказываются излишне ресурсоёмкими, мы имеем явное расхождение требуемого с результатом исполнения. Особенно критично это в случаях небиблиотечного контролирования переходных процессов. К примеру: выполнения перехода по кубической кривой (easing), или работы с ритмичным вызовом логики приложения для обновления текущего состояния. Пару месяцев назад, в качестве «weekend project», я выбрал для себя написание простого пошагового секвенсера (wiki), и столкнулся с физической невозможностью точного тайминга на среднеслабых и слабых системах посредством стандартных setTimeout() и setInterval(). Рассогласование достигало непримиримых в этом случае полусекунд. В поисках решения, я наткнулся на отличную статью по этой теме. А сам пост, в некоем роде, — вольный перевод оной.

В итоге, задача «точного» тайминга сводится к вычитанию задержки предыдущего выполнения функции из настоящего. Можно просто измерить разницу в системном времени между итерациями и вычесть её при следующем вызове. Звучит просто, а вот и код:

var start = new Date().getTime(),     time = 0,     elapsed = '0.0';  function instance() {     time += 100;      elapsed = Math.floor(time / 100) / 10;     if(Math.round(elapsed) == elapsed) { elapsed += '.0'; }      document.title = elapsed;      var diff = (new Date().getTime() - start) - time;     window.setTimeout(instance, (100 - diff)); }  window.setTimeout(instance, 100); 

Все довольно просто, посмотрим на результаты. Вот демо на JSfiddle с комментариями на русском языке, для сравнения работы обычных таймеров и таймеров с автокоррекцией.
Лучшее в таком подходе то, что не имеет практического значения насколько неточен таймер, т.к. впоследствии небольшая постоянноя задержка (как 3-4ms в последнем примере демо), может быть очень легко компенсирована. В то время, как неточность простого таймера носит куммулятивный характер, накапливаясь с каждой итерацией, что в конце приводит к адски заметной разнице.

Как было сказано выше, с данной проблемой я столкнулся, при написании аудио-приложения. Приложения, это конечно очень громко сказано, скорее просто очередного учебного мини-проекта. После изучения материала был написан вот этот код:

//по нажатию на кнопку "play/stop", срабатывает функция включающая таймер         function preciousTimer (step) {  //как и в примерах выше, берем DateStamp для оценки             var start = new Date().getTime(),                 time = 0, /*а эта переменная появилась из необходимости  проводить в четное количество раз больше итераций, чем шагов в секвенсере (точность все еще довольно слабенькая)*/                 it = 0;              function instance () {  //рассчитываем идеальное время                 time += step;  //считаем разницу                 var diff = (new Date().getTime()- start) - time;  //выполняем согласно значению итератора                 if (it == 4) {                     it = 0; /*место для работы секвенсера с матрицей, здесь смотрим значения логического массива для каждого прохода по планке. */                           if (m == 8) {                         m = 0;                     };                     for (var i = 0; i < 4; i++) {                         if (noteArr[i][m]) {                             sound[i].play();                         };                     };                     m++;                 };                     it++;  //если за время итерации была нажата кнопка паузы,  //выходим из хвостовой рекурсивной цепочки                     if (pause) {                          return;                      };  //вызываем следующую итерацию, с учетом задержки                     window.setTimeout(instance, (step - diff));                 };  //а это самый первый вызов функции instance(),  //после которого начинается последовательный вызов итераций             setTimeout(instance, step);         }; 

Кто-то уже наверняка задался вопросом: а как же быть с переполнением стека вызовов. В данном конкретном случае, его размер колеблется от 10 до 17 позиций, что мало для любого современного браузера. Однако с увеличением темпа, либо вместе с ростом количества перерасчетных итераций, может случится и приступ удушья у оного и необходимо будет задуматься о реализации .tail() — подобных вызовов. Но об этом уже совсем другая история.

Также нельзя не упомянуть про метод window.performance.now(), который возвращает число с плавающей запятой, значащее количество миллисекунд, прошедшее с загрузки страницы (не совсем точное определение) Следовательно после десятичной запятой у нас будет уже субмиллисекундное разрешение, что очень очень хорошо. Используя это значение, можно по схожему методу вычислить рассогласование с точностью до десятой доли миллисекунды, и более точно выполнить предзапуск последующей итерации.

Посмотреть секвенсер вживую можно здесь: stepograph.hol.es (webkit required)
Cсылка на оригинал статьи, частично используемой в посте: Сreating accurate timers in JavaScript

Спасибо за внимание!

ссылка на оригинал статьи http://habrahabr.ru/post/212889/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *