В посте в повествовательной и не очень манере рассказывается о различных реализациях «точных» таймеров на 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/
Добавить комментарий