Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

Таймеры: "setTimeout" и "setInterval"

  1. setTimeout
    1. Параметры для функции и контекст
    2. Отмена исполнения
  2. setInterval
  3. Реальная пауза в setInterval
  4. Повторение с гарантированной задержкой
  5. Минимальная задержка таймера
  6. Трюк setTimeout(func, 0)
  7. setTimeout для разбивки долгих скриптов
  8. Итого

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

setTimeout

Синтаксис:

var timerId = setTimeout(функция или строка кода, задержка)

Параметры:

Функция или строка кода
Функция или строка кода для исполнения. Использовать строку кода не рекомендуется.
Задержка
Задержка в милисекундах, 1000 милисекунд равны 1 секунде.

Исполнение функции начнётся через заданное время задержки.

Например, следующий код вызовет alert('Привет') через одну секунду:

function func() { 
  alert('Привет');
}
setTimeout(func, 1000);

Если первый аргумент является строкой, то интерпретатор создаёт анонимную функцию из этой строки.

То есть такая запись работает точно так же.

setTimeout("alert('Привет')", 1000);

Использование строк не рекомендуется, так как они могут вызвать проблемы при минимизации кода. Сама возможность использовать строку сохраняется лишь для совместимости.

Вместо них используйте анонимные функции:

setTimeout(function() { alert('Привет') }, 1000);

Параметры для функции и контекст

По умолчанию, setTimeout не передаёт функции никаких аргументов. Пример ниже выведет undefined:

function sayHi(who) {
  alert("Привет, я " + who);
}

setTimeout(*!*sayHi*/!*, 1000);

Для того, чтобы аргументы передать, обёртывают вызов в анонимную функцию:

function sayHi(who) {
  alert("Привет, я " + who);
}

setTimeout(*!*function() { sayHi('Вася') }*/!*, 1000);

Прямой вызов метода объекта через setTimeout также работать не будет, т.к. он не передаёт this.

Например:

function User(id) {
  this.id = id;

  this.sayHi = function() {
    alert(this.id);
  };
}

var user = new User(12345);

*!*
setTimeout(user.sayHi, 1000); // выведет "undefined"
*/!*

Здесь функция user.sayHi выполняется в глобальном контексте, т.к. setTimeout получает функцию из объекта и планирует её на выполнение.

Иначе говоря, эти два вызова setTimeout делают одно и то же:

setTimeout(user.sayHi, 1000); // 1

var func = user.sayHi;
setTimeout(func, 1000); // 2

Эта проблема также решается созданием промежуточной функции:

function User(id) {
  this.id = id;

  this.sayHi = function() {
    alert(this.id);
  };
}

var user = new User(12345);

*!*
setTimeout(function() {
  user.sayHi( /* аргументы, если нужны */ );
}, 1000);  
*/!*

В следующих главах мы разберём дополнительные способы привязки функции к объекту.

Отмена исполнения

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

Синтаксис: clearTimeout(timerId).

В следующем примере мы ставим таймаут, а затем удаляем (передумали). В результате ничего не происходит.

var timerId = setTimeout(function() { alert(1) }, 1000);

clearTimeout(timerId);

setInterval

Метод timerId = setInterval(функция|код, задержка) имеет синтаксис, аналогичный setTimeout.

Но, в отличие от setTimeout, он запускает повторение функции каждый раз через указанный интервал времени задержка.

Остановить исполнение можно вызовом clearInterval(timerId).

Следующий пример при запуске станет выводить сообщение каждые две секунды, пока вы не нажмете на кнопку «Стоп»:

<input type="button" onclick="clearInterval(timer)" value="Стоп">

<script> 
  var i = 1;
  var timer = setInterval(function() { alert(i++) }, 2000);
</script>

Реальная пауза в setInterval

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

На самом деле пауза между вызовами функции в setInterval всегда меньше, чем задержка.

Для примера, возьмем setInterval(function() { func(i++) }, 100). Она выполняет func каждые 100 мс, каждый раз увеличивая значение счетчика.

На картинке ниже, красный блок - это время исполнения func. Время между блоком — это время между запусками функции, и оно меньше, чем установленная задержка!

То есть, браузер инициирует запуск функции аккуратно каждые 100мс, без учета времени выполнения самой функции.

Бывает, что исполнение функции занимает больше времени, чем задержка. Например, функция сложная, а задержка маленька. Или функция содержит операторы alert/confirm/prompt, которые блокируют поток выполнения. В этом случае начинаются интересные вещи Smile

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

Изображение ниже иллюстрирует происходящее для функции, которая долго исполняется.

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

Второй запуск функции происходит сразу же после окончания первого:

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

На изображении ниже setInterval пытается выполнить функцию в 200 мс и ставит вызов в очередь. В 300 мс и 400 мс таймер пробуждается снова, но ничего не просходит.

Модальные окна в Safari/Chrome блокируют таймер

Браузеры Safari/Chrome ведут себя по-другому. Внутренний таймер во время показа alert/confirm/prompt в них «не тикает». Сколько бы не висело модальное окно — новые вызовы функций просто будут отложены, как будто время «заморозилось».

Поэтому пример ниже не воспроизводится в этих браузерах. Это баг.

Давайте рассмотрим на примере, как это работает.

Запустите пример ниже в любом браузере, кроме Chrome/Safari и дождитесь всплывающего окна. Обратите внимание, что это alert. Пока модальное окошко отображается, исполнение JavaScript блокируется. Подождите немного и нажмите OK.

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

<input type="button" onclick="clearInterval(timer)" value="Стоп">

<script> 
  var i = 1;
  timer = setInterval(function() { alert(i++) }, 2000);
</script>

Происходит следующее.

  1. Браузер выполняет функцию каждые 2 секунды
  2. Когда всплывает окно alert — исполнение блокируется и остается заблокированным всё время, пока alert отображается.
  3. Если вы ждете достаточно долго, то внутренние часики-то идут. Браузер ставит следующее исполнение в очередь, один раз (в Chrome/Safari внутренний таймер не идёт! это баг.).
  4. Когда вы нажмёте OK - моментально вызывается исполнение, которые было в очереди.
  5. Следующее исполнение вызовется с меньшей задержкой, чем указано. Так происходит потому, что планировщик просыпается каждые 2000мс. И если alert был закрыт через 3500мс, то следующее исполнение будет на 4000 мс, т.е. через 500мс.

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

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

Повторение с гарантированной задержкой

В случаях, когда нужна фиксированная задержка между запусками, setInterval не подойдет.

Вместо него используется повторная установка setTimeout по завершению каждого запуска функции.

Ниже — пример, который выдает alert с интервалами 2 секунды между ними.

<input type="button" onclick="clearTimeout(timer)" value="Стоп">

<script> 
  var i = 1;

  var timer = setTimeout(function run() { 
    alert(i++);
    timer = setTimeout(run, 2000);
  }, 2000);

</script>

На временной линии выполнения будут фиксированные задержки между запусками. Иллюстрация для задержки 100мс:

Дополнительное преимущество использования setTimeout вместо setInterval заключается в том, что функция сама решает, запланировать свой новый запуск или нет.

Минимальная задержка таймера

У браузерного таймера есть минимальная возможная задержка. Она меняется от примерно нуля до 4мс в современных браузерах. В более старых она может быть больше и достигать 15мс.

По стандарту, минимальная задержка для setTimeout — 4мс, для setInterval — 10мс.

Так как минимально возможное разрешение — 4мс, то нет разницы между setTimeout(..,1) и setTimeout(..,4).

Посмотреть минимальное разрешение «вживую» можно на следующем примере.

В примере ниже находятся DIV'ы, каждый удлиняется вызовом setInterval с указанной в нём задержкой — от 0мс (сверху) до 20мс (внизу).

Запустите его в различных браузерах, в частности, в Chrome и Firefox. Вы наверняка заметите, что несколько первых DIV'ов анимируются одинаково. Это как раз потому, что слишком маленькие задержки таймер не различает.

Открыть в новом окне Открыть в песочнице

В поведении setTimeout и setInterval с нулевой задержкой есть браузерные особенности.

  • В Opera, setTimeout(.., 0) — то же самое, что setTimeout(.., 4). Оно выполняется реже, чем setTimeout(.. ,2). Это особенность данного браузера.
  • В Internet Explorer, нулевая задержка setInterval(.., 0) не сработает. Это касается именно setInterval, т.е. setTimeout(.., 0) работает нормально.

Пример ниже реализует такую же анимацию, но через setTimeout.

Если посмотреть его в различных браузерах, то заметить отличия от setInterval.

Обратите внимание, если 4мс для setTimeout браузеры более-менее соблюдают, то 10мс для setInterval — нет.

Открыть в новом окне Открыть в песочнице

Трюк setTimeout(func, 0)

Когда функция setTimeout получает 0 в качестве последнего аргумента, она стремится выполнить func как можно скорее. Но сразу же она сделать это не может — таймаут, всё же, должен быть.

Поэтому func оборачивают в setTimeout(func, 0), если хотят что-то сделать сразу после выполнения скрипта.

Например:

var result = 0;

function showResult() {
  alert(result);
}

*!*
setTimeout(showResult, 0);
*/!*

result = 2*2;

// выведет 4

setTimeout для разбивки долгих скриптов

Нулевой или небольшой таймаут также используют, чтобы разорвать поток выполнения «тяжелых» скриптов.

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

Браузер сначала будет есть 100% процессора, а затем может выдать сообщение о том, что скрипт выполняется слишком долго.

Для того, чтобы этого избежать, сложная задача разбивается на части.

Как только закончится одна часть — через небольшое время по setTimeout назначается другая. В этот промежуток браузер «отдыхает», освобождает процессор и реагирует на другие события.

Итого

Методы setInterval(func, delay) и setTimeout(func, delay) позволяют запускать func регулярно/один раз через delay миллисекунд.

Оба метода возвращают идентификатор таймера. Его используют для остановки выполнения вызовом clearInterval/clearTimeout.

Особенности
setInterval setTimeout
Тайминг Идет вызов строго по таймеру. Если интерпретатор занят — один вызов становится в очередь.

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

Рекурсивный вызов setTimeout используется вместо setInterval там, где нужна фиксированная пауза между выполнениями.
Задержка Минимальная задержка по стандарту составляет 10мс. Минимальная задержка по стандарту составляет 4мс.
Минимальная задержка для этих методов в современных браузерах различна и колеблется от примерно нуля до 4мс. В старых браузерах она может доходить до 15мс.
Браузерные особенности В IE не работает задержка 0. В Opera нулевая задержка эквивалентна 4мс, остальные задержки обрабатываются точно, в том числе нестандартные 1мс, 2мс и 3мс.

Напишите функцию delay(f, ms), которая возвращает обёртку вокруг f, задерживающую вызов на ms миллисекунд.

Например:

function f(x) {
  alert(x);
}

var f1000 = delay(f, 1000);
var f1500 = delay(f, 1500);

f1000("тест"); // выведет "тест" через 1000 миллисекунд
f1500("тест2"); // выведет "тест2" через 1500 миллисекунд

Иначе говоря, f1000 — это «задержанный на 1000мс» вызов f.

Решение
Решение

*!*
function delay(f, ms) {

  return function() {
    var _this = this, _arguments = arguments;
    setTimeout(function() {
      f.apply(_this, _arguments);
    }, ms);
  };

}
*/!*

function f(x) {
  alert(x);
}

var f1000 = delay(f, 1000);
var f1500 = delay(f, 1500);

f1000("тест"); // выведет "тест" через 1000 миллисекунд
f1500("тест2"); // выведет "тест2" через 1500 миллисекунд

Напишите функцию debounce(f, ms), которая возвращает обёртку вокруг f, которая допускает вызов f не чаще, чем раз в ms миллисекунд.

Например:

function f() { ... }

var f1000 = debounce(f, 1000);

f1000(); // выполнится сразу же
f1000(); // игнор

setTimeout( f1000, 100); // игнор (прошло только 100мс)
setTimeout( f1000, 1100); // выполнится

Вызовы, которые происходят в течение таймаута, игнорируются.

Исходный документ с тестом: tutorial/timers/debounce-src.html

Решение
Решение

Решение: tutorial/timers/debounce.html

Вызов debounce возвращает функцию-обёртку. Все необходимые данные для неё хранятся в замыкании.

При вызове ставится таймер и состояние state меняется на константу COOLDOWN («в процессе охлаждения»).

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

Напишите функцию throttle(f, ms), которая возвращает обёртку вокруг f, вызывающую f не чаще, чем раз в ms миллисекунд.

Чтобы лучше понять, как она должна работать — разберём реальное применение.

Например, нужно обрабатывать передвижение мыши. JavaScript позволяет указать функцию, которая будет запускаться при каждом передвижении мыши и получать координаты курсора. Эта функция запускается очень часто, может быть 100 раз в секунду (каждые 10мс).

Во время передвижения необходимо обновлять информацию на странице. Но делать это целесообразно не чаще чем раз в 100мс, во-первых чтобы не грузить процессор, во-вторых чтобы не было лишнего мигания.

Назовём функцию, которая осуществляет обновление, update.

Эта задача должна решаться вызовом var update100 = throttle(update, 100).

Такая обёртка будет:

  1. Передавать вызов update(...) при первом запуске update100(...).
  2. Следующий вызов, если он раньше чем через 100мс — запланировать.
  3. Дальнейшие вызовы игнорировать (уже запланирован).
  4. По истечении 100мc, если были вызовы, то запустить update(...) ещё 1 раз, передав ей последние аргументы.

Обработка перемещений будет работать так:

  1. Первое обновление произойдёт сразу (это важно, посетитель тут же видит реакцию на своё действие).
  2. Дальше будет много вызовов (микро-передвижений) с разными координатами, но пока не пройдёт 100мс — ничего не будет.
  3. По истечении 100мс — опять обновление, с последними координатами. Промежуточные микро-передвижения игнорированы.

Принципиально важно, что и первое и последнее передвижение обрабатываются, в отличие от функции debounce, которая игнорирует всё, что попало в таймаут. То есть, когда посетитель остановит мышь — возможно, с небольшой задержкой, но её последняя позиция будет обработана.

Чтобы было удобнее такой throttle писать, в исходном документе содержатся еще два теста: tutorial/timers/throttle-src.html

Решение
Решение

Решение: tutorial/timers/throttle.html

Вызов throttle возвращает функцию-обёртку. Все необходимые данные для неё хранятся в замыкании.

В первую очередь — это состояние state, которое вначале не назначено (null), при первом вызове получает значение COOLDOWN («в процессе охлаждения»), а при следующем вызове CALL_SCHEDULED.

При таймауте состояние проверяется, и если оно равно CALL_SCHEDULED — происходит новый вызов.

См. также:

Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
  3. Комментарии без смысла, с рекламой или не о статье вообще - удаляются.
Наверх

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!