Асинхронные таймеры являются краеугольным камнем всех основанных на времени процессов в JavaScript. От очевидных вещей, таких как часы и секундомеры, до визуальных эффектов и анимации, до синхронизированных задержек, которые жизненно важны для удобства использования выпадающих меню .
Но проблема с таймерами JavaScript в том, что они не очень точны . Мы не могли сделать секундомер, просто увеличивая х , потому что он не задержался бы во времени:
var time = 0,
elapsed = '0.0';
window.setInterval(function()
{
time += 100;
elapsed = Math.floor(time / 100) / 10;
if(Math.round(elapsed) == elapsed) { elapsed += '.0'; }
document.title = elapsed;
}, 100);
Веб-браузеры, как и все приложения, по очереди занимают часть процессорного времени, и время, которое им приходится ждать, зависит от нагрузки. Это то, что вызывает задержку в асинхронных таймерах — таймер 200 мс может фактически занять 202 мс или 204, и это будет постепенно посылать секундомер вне времени.
Решение в этом случае заключается не в том, чтобы вообще полагаться на скорость таймера, а в том, чтобы заново запросить системное время в каждом цикле и получить вывод из этого:
var start = new Date().getTime(),
elapsed = '0.0';
window.setInterval(function()
{
var time = new Date().getTime() - start;
elapsed = Math.floor(time / 100) / 10;
if(Math.round(elapsed) == elapsed) { elapsed += '.0'; }
document.title = elapsed;
}, 100);
Но как насчет менее буквальных приложений, таких как анимация — можем ли мы использовать тот же подход, чтобы сделать их такими же точными?
Не могу победить систему
Недавно я работал над некоторыми визуальными переходами, и я задумался над именно этим вопросом: если пользователь указывает 5-секундную анимацию, что мы можем сделать, чтобы эта анимация действительно длилась 5 секунд, а не 5.1 или 5.2? Это может быть небольшая разница, но небольшие различия складываются. И в любом случае, улучшение вещей — самоцель!
Итак, чтобы перейти к погоне — мы действительно можем использовать системные часы, чтобы компенсировать неточность таймера. Если мы запускаем анимацию как серию вызовов setTimeout
определить, насколько она неточна, и вычесть это различие из следующей итерации:
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);
Чтобы увидеть практический эффект всего этого, я подготовил демонстрацию, которая иллюстрирует разницу между обычными и отрегулированными таймерами — примеры саморегулирующихся таймеров .
Демо имеет три примера:
- Первый пример (слева) — это просто обычный таймер, реализованный с помощью
setInterval
- Второй пример (по центру) увеличивает объем работы, выполняемой браузером на каждой итерации, чтобы показать, как больше работы означает большую задержку и, следовательно, гораздо большую неточность;
- Третий пример (справа) выполняет ту же работу, что и второй, но теперь использует технику самонастройки, чтобы показать, насколько это существенно влияет на общую точность;
Самое замечательное в этом подходе состоит в том, что на самом деле не имеет значения, насколько неточным будет таймер, настройки всегда будут держать его во времени. Небольшая величина постоянной задержки будет легко компенсирована, но в равной степени легко модерируется внезапный большой всплеск задержки, вызванный всплеском использования процессора (например, запуском приложения). И мы можем видеть из нескорректированных примеров, что, хотя неточность в одну итерацию достаточно мала, совокупный эффект может быть невероятно большим.
Конечно, даже настроенный таймер не может компенсировать 100% — он регулирует скорость следующей итерации и поэтому не может компенсировать задержку последней итерации. Но тем не менее, какой бы ни была эта разница, она будет крошечной по сравнению с совокупным эффектом этого несоответствия, умноженным на сотни или тысячи случаев.
Плыть по течению
Ранее я говорил, что у меня возникла эта идея во время работы над анимацией (для обновления моих популярных переходов изображений ), и ниже приводится абстракция, с которой я в конце концов придумал. Он реализует самонастраивающийся таймер, вычисляет скорость и шаги по длине и разрешению ввода и обеспечивает обратные вызовы для экземпляра и при завершении:
function doTimer(length, resolution, oninstance, oncomplete)
{
var steps = (length / 100) * (resolution / 10),
speed = length / steps,
count = 0,
start = new Date().getTime();
function instance()
{
if(count++ == steps)
{
oncomplete(steps, count);
}
else
{
oninstance(steps, count);
var diff = (new Date().getTime() - start) - (count * speed);
window.setTimeout(instance, (speed - diff));
}
}
window.setTimeout(instance, speed);
}
Вот упрощенный пример его использования, который приводит к исчезновению непрозрачности изображения (используя только стандартный синтаксис) в течение 5 секунд со скоростью 20 кадров в секунду — Саморегулирующееся исчезновение непрозрачности :
var img = document.getElementById('image');
var opacity = 1;
img.style.opacity = opacity;
doTimer(5000, 20, function(steps)
{
opacity = opacity - (1 / steps);
img.style.opacity = opacity;
},
function()
{
img.style.opacity = 0;
});
Вот и все — саморегулирующиеся таймеры улучшают анимацию, давая вам уверенность в том, что при указании 5-секундного эффекта вы получите тот, который длится 5 секунд!
Миниатюра: Юкон Белый Свет