Всё только о JavaScript

/ Статьи / Отложенные вычисления

Чем плох setInterval

Для многократного запуска кода через равные промежутки времени предназначена функция setInterval. Тем не менее она обладает рядом минусов, в основном это разное поведение в различных браузерах.

Первое отличие состоит в разности момента установки таймера для следующего запуска. Создадим небольшой тест: будем отмерять количество времени, прошедшее с начала выполнения предыдущего запуска и с его окончания.

var d1 = new Date(), d2 = new Date();
setInterval(function() {
    var d = new Date();
    document.body.innerHTML += (d - d1) + ' ' + (d - d2) + '<br>';

    // Ставим метку в начале функции
    d1 = new Date();
    while (new Date() - d1 < 200);    // ничего не делаем 200 миллисекунд
    // И в конце функции
    d2 = new Date();
}, 1000);

Вывод будет информативным, начиная со второй строчки.

В Firefox, Opera, Safari и Chrome ситуация будет схожая: первое число будет примерно равно 1000, второе — на 200 меньше. Различие будут только в разбросе значений. Самый маленький разброс в Chrome и Opera.

1000 800
1003 803
1004 804
1018 808
987 787
1003 803
1000 800

Чуть больше разброс в Safari.

1035 835
1259 1059
841 641
1091 891
1048 848
1005 805
998 798

И самый большой — в Firefox.

988 788
985 785
1007 807
971 771
984 784
990 790
974 774
1010 810
1004 804

В Internet Explorer ситуация более стабильная, но противоположная: 1000 равно второе число, а первое на 200 больше.

1203 1000
1203 1000
1203 1000
1203 1000
1204 1000
1203 1000
1203 1000

Другими словами IE устанавливает таймер на следующий запуск после выполнения callback-функции, а остальные браузеры — до. Поэтому, например, анимация, реализованная с помощью setInterval в Internet Explorer всегда будет медленнее, чем в других браузерах.

Ещё одно отличие менее заметное и более трудновоспроизводимое, но иногда способное доставить много хлопот, — это устойчивость к изменению системного времени. Если запустить следующий тест

setInterval(function() {
    document.body.innerHTML = Math.random();
}, 500);

И после запуска перевести системное время на минуту назад, то в браузерах Firefox и Safari смена чисел приостановится, а через минуту запустится вновь. Конечно, ручной перевод системного времени крайне редкая ситуация, но на многихсистемах настроена автоматическая синхронизация времени с серверами в интернете, поэтому в некоторых ситуациях нельзя сбрасывать со счетов этот фактор.

Ещё один маленький минус функции setInterval — чтобы была возможность остановить её действие, необходимо где-то запоминать её идентификатор, что не всегда удобно.

Чем заменить setInterval

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

(function() {
    // Выполняем периодические действия

    setTimeout(arguments.callee, 500);
})();

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

/**
 * @param {Function} callback Фукнция, вызываемая по окончании работы.
 */
function foo(callback) {
    // Инициализируем переменные

    (function() {
        // Выполняем действия

        if (/* больше ничего делать не надо */) {
            callback();
        } else {
            setTimeout(arguments.callee, 500);
        }
    })();
}

Обратите внимание, в отличие от setInterval, в данном случае первая итерация выполняется сразу же, без задержки.

Напишем, например, функцию, посимвольно выводящую строку в нужный DOM-элемент, и вызывающую callback-функцию по окончанию работы.

function typeString(elId, str, callback) {
    var i = 1, el = document.getElementById(elId);
    (function() {
        el.innerHTML = str.substr(0, i++);
        if (i > str.length) {
            callback();
        } else {
            setTimeout(arguments.callee, 300);
        }
    })();
}

typeString('t', 'Hello, World!', function() {
    alert('Напечатали');
});