Конспект JS-course

JavaScript Garden: setTimeout и setInterval

Источник: http://bonsaiden.github.io/JavaScript-Garden/ru/#other.timeouts

setTimeout и setInterval

Поскольку JavaScript поддерживает асинхронность, есть возможность запланировать выполнение функции, используя функции setTimeout и setInterval.

function foo() {}
var id = setTimeout(foo, 1000); // возвращает число > 0

Функция setTimeout возвращает идентификатор таймаута и планирует вызвать foo через, примерно, тысячу миллисекунд. Функция foo при этом будет вызвана ровно один раз.

В зависимости от разрешения таймера в используемом для запуска кода движке JavaScript, а также с учётом того, что JavaScript является однопоточным языком и посторонний код может заблокировать выполнение потока, нет никакой гарантии, что переданный код будет выполнен ровно через указанное в вызове setTimeout время.

Замечание: Поскольку setTimeout принимает объект функции в качестве первого параметра, часто совершается ошибка в использовании setTimeout(foo(), 1000), при котором будет использоваться возвращённое значение от вызова функции foo, а не вызываться сама функция foo. В большинстве случаев ошибка пройдёт незамеченной, а в случае если функция возвращает undefined, setTimeout вообще не породит никакой ошибки.

Переданная первым параметром функция будет вызвана как глобальный объект — это значит, что оператор this в вызываемой функции будет ссылаться на этот самый объект.

function Foo() {
    this.value = 42;
    this.method = function() {
        // this ссылается на глобальный объект
        console.log(this.value); // выведет в лог undefined
    };
    setTimeout(this.method, 500);
}
new Foo();

Поочерёдные вызовы с использованием setInterval

setTimeout вызывает функцию единожды; setInterval — как и предполагает название — вызывает функцию каждые X миллисекунд. И его использование не рекомендуется.

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

function foo(){
    // что-то, что выполняется одну секунду
}
setInterval(foo, 100);

В приведённом коде foo выполнится один раз и заблокирует этим главный поток на одну секунду.

Пока foo блокирует код, setInterval продолжает планировать последующие её вызовы. Теперь, когда первая foo закончила выполнение, в очереди будут уже десять ожидающих выполнения вызовов foo.

Функцию setInterval лучше использовать, если вы стопроцентно уверены, что код внутри неё будет исполняться как минимум в три раза быстрее переданного ей интервала.

Разбираемся с потенциальной блокировкой кода

Самый простой и контролируемый способ — использовать setTimeout внутри самой функции.

function foo(){
    // что-то, выполняющееся одну секунду
    setTimeout(foo, 100);
}
foo();

Такой способ не только инкапсулирует вызов setTimeout, но и предотвращает от очередей блокирующих вызовов и при этом обеспечивает дополнительный контроль. Сама функция foo теперь принимает решение, хочет ли она запускаться ещё раз или нет.

Очистка таймаутов вручную

Удаление таймаутов и интервалов работает через передачу соответствующего идентификатора либо в функцию clearTimeout, либо в функцию clearInterval — в зависимости от того, какая функция set... использовалась для его получения.

var id = setTimeout(foo, 1000);
clearTimeout(id);

Очистка всех таймаутов

Из-за того, что встроенного метода для удаления всех таймаутов и/или интервалов не существует, для достижения этой цели приходится использовать брутфорс.

// удаляем "все" таймауты
for(var i = 1; i < 1000; i++) {
    clearTimeout(i);
}

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

Скрытое использование eval

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

function foo() {
    // будет вызвана
}

function bar() {
    function foo() {
        // никогда не будет вызвана
    }
    setTimeout('foo()', 1000);
}
bar();

Поскольку eval в этом случае не вызывается напрямую, переданная в setTimeout строка будет выполнена в глобальной области видимости; так что локальная переменная foo из области видимости bar не будет выполнена.

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

function foo(a, b, c) {}

// НИКОГДА не делайте такого
setTimeout('foo(1,2, 3)', 1000)

// Вместо этого используйте анонимную функцию
setTimeout(function() {
    foo(1, 2, 3);
}, 1000)

Замечание: При том, что синтаксис setTimeout(foo, 1000, 1, 2, 3) разрешено использовать, это крайне не рекомендуется, поскольку может привести к сложно распознаваемым ошибкам при работе с методами.

Заключение

Никогда не используйте строки как параметры setTimeout или setInterval. Это явный признак действительно плохого кода. Если вызываемой функции необходимо передавать аргументы, лучше передавать анонимную функцию, которая самостоятельно будет отвечать за сам вызов.

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