Картинка к посту – это работа Сальвадора Дали «Время течет». Этот выбор отнюдь не случаен и метафоричен по своей сути.
В рамках программирования на JS время может течь не совсем так, как мы это предполагаем. JavaScript однопоточен. Это значит, что различные функции выполняются в определенной фиксированной последовательности.
Но некоторые этапы вычисления могут оказаться очень ресурсоёмкими и занять больше времени, чем требуется. Например, на 2-3 мс больше. И эта неточность постоянно накапливается. Особенно критично это в случаях контролирования переходных процессов. К примеру, выполнения перехода с изменением координаты во времени по кубической кривой (easing) или работы с ритмичным вызовом логики приложения для обновления текущего состояния.
Я сам столкнулся с физической невозможностью точного тайминга посредством стандартных IR.SetTimeout() и IR.SetInterval() пару месяцев назад, работая над небольшим проектом. Рассогласование достигало неприемлемых в этом случае 0,5 сек.
Джеймс Эдвардс, фрилансер веб-разработчик, специализирующийся на разработке JavaScript приложений, предлагает решить задачу «точного» тайминга путем вычитания задержки предыдущего выполнения функции из настоящего. Можно просто измерить разницу в системном времени между итерациями и вычесть её при следующем вызове. В результате Джеймс Эдвардс предложил следующий код для решения данной задачи:
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’; }
var diff = (new Date().getTime() — start) — time;
IR.SetTimeout((100 — diff), instance);
}
IR.SetTimeout(100, instance,);
Это довольно простое, но хорошее решение. Преимущество такого подхода в том, что неважно насколько неточен таймер, так как впоследствии небольшая постоянная задержка (3-4 мс) может быть легко компенсирована. В то время как неточность простого таймера носит кумулятивный характер, накапливаясь с каждой итерацией, что в итоге приводит к очень заметной разнице.
Как было сказано выше, с проблемой неточных таймеров я столкнулся при написании проекта для работы с аудио. После глубокого изучения материалов по созданию корректных таймеров в JS и на основе кода, приведенного выше, был написан вот этот код:
//по нажатию на кнопку «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;
};
//вызываем следующую итерацию, с учетом задержки
IR.SetTimeout((step — diff), instance);
};
//а это самый первый вызов функции instance(),
//после которого начинается последовательный вызов итераций
IR.SetTimeout(step, instance);
};
Написанный мной код позволяет воспроизводить музыку все время, без задержки. И это лишь один из вариантов использования подхода, предложенного Джеймсом Эдвардсом. На его основе вы можете самостоятельно написать точные таймеры с автокоррекцией для любых целей.
Оригинал статьи, частично используемой в посте, вы можете прочитать здесь: Сreating accurate timers in JavaScript
Илья Марков,
скрипт-программист iRidium mobile