Статьи

Создание точных таймеров в JavaScript

Асинхронные таймеры являются краеугольным камнем всех основанных на времени процессов в 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);

Чтобы увидеть практический эффект всего этого, я подготовил демонстрацию, которая иллюстрирует разницу между обычными и отрегулированными таймерами — примеры саморегулирующихся таймеров .

Демо имеет три примера:

  1. Первый пример (слева) — это просто обычный таймер, реализованный с помощью setInterval
  2. Второй пример (по центру) увеличивает объем работы, выполняемой браузером на каждой итерации, чтобы показать, как больше работы означает большую задержку и, следовательно, гораздо большую неточность;
  3. Третий пример (справа) выполняет ту же работу, что и второй, но теперь использует технику самонастройки, чтобы показать, насколько это существенно влияет на общую точность;

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

Конечно, даже настроенный таймер не может компенсировать 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 секунд!

Миниатюра: Юкон Белый Свет