В посте в повествовательной и не очень манере рассказывается о различных реализациях «точных» таймеров на 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)
Ссылка на оригинал статьи, частично используемой в посте: Сreating accurate timers in JavaScript
Спасибо за внимание!