Мы можем решить выполнить функцию не прямо сейчас, а через какое-то время. Это называется «запланировать звонок».
Для этого есть два метода:
setTimeout
позволяет нам запустить функцию один раз по истечении определенного интервала времени.
setInterval
позволяет нам запускать функцию повторно, начиная с определенного интервала времени, а затем непрерывно повторяя его через этот интервал.
Эти методы не являются частью спецификации JavaScript. Но большинство сред имеют внутренний планировщик и предоставляют эти методы. В частности, они поддерживаются во всех браузерах и Node.js.
Синтаксис:
let timerId = setTimeout(func|code, [задержка], [arg1], [arg2], ...)
Параметры:
func|code
Функция или строка кода для выполнения. Обычно это функция. По историческим причинам можно передать строку кода, но это не рекомендуется.
delay
Задержка перед запуском в миллисекундах (1000 мс = 1 секунда), по умолчанию 0.
arg1
, arg2
…
Аргументы для функции
Например, этот код вызывает sayHi()
через одну секунду:
функция SayHi() { Предупреждение('Привет'); } setTimeout (скажем Привет, 1000);
С аргументами:
функция SayHi(фраза, кто) { alert(фраза + ', ' + кто ); } setTimeout(sayHi, 1000, «Привет», «Джон»); // Привет, Джон
Если первый аргумент — строка, то JavaScript создаёт из неё функцию.
Итак, это также будет работать:
setTimeout("alert('Привет')", 1000);
Но использовать строки не рекомендуется, используйте вместо них стрелочные функции, например:
setTimeout(() => alert('Привет'), 1000);
Передайте функцию, но не запускайте ее
Начинающие разработчики иногда допускают ошибку, добавляя скобки ()
после функции:
// неправильный! setTimeout (sayHi (), 1000);
Это не работает, поскольку setTimeout
ожидает ссылку на функцию. И здесь sayHi()
запускает функцию, и результат ее выполнения передается в setTimeout
. В нашем случае результат sayHi()
undefined
(функция ничего не возвращает), поэтому ничего не запланировано.
Вызов setTimeout
возвращает «идентификатор таймера» timerId
, который мы можем использовать для отмены выполнения.
Синтаксис для отмены:
пусть timerId = setTimeout(...); ClearTimeout (идентификатор таймера);
В приведенном ниже коде мы планируем выполнение функции, а затем отменяем ее (передумали). В результате ничего не происходит:
let timerId = setTimeout(() => alert("никогда не бывает"), 1000); оповещение (идентификатор таймера); // идентификатор таймера ClearTimeout (идентификатор таймера); оповещение (идентификатор таймера); // тот же идентификатор (не становится нулевым после отмены)
Как мы видим из вывода alert
, в браузере идентификатор таймера представляет собой число. В других средах это может быть что-то другое. Например, Node.js возвращает объект таймера с дополнительными методами.
Опять же, для этих методов не существует универсальной спецификации, так что это нормально.
Для браузеров таймеры описаны в разделе таймеров HTML Living Standard.
Метод setInterval
имеет тот же синтаксис, что и setTimeout
:
let timerId = setInterval(func|code, [задержка], [arg1], [arg2], ...)
Все аргументы имеют одно и то же значение. Но в отличие от setTimeout
он запускает функцию не один раз, а регулярно через заданный интервал времени.
Чтобы остановить дальнейшие вызовы, мы должны clearInterval(timerId)
.
В следующем примере сообщение будет отображаться каждые 2 секунды. Через 5 секунд вывод прекращается:
// повторяем с интервалом в 2 секунды let timerId = setInterval(() => alert('тик'), 2000); // через 5 секунд остановка setTimeout(() => {clearInterval(timerId); alert('stop'); }, 5000);
Время идет, пока отображается alert
В большинстве браузеров, включая Chrome и Firefox, внутренний таймер продолжает «тикать», отображая alert/confirm/prompt
.
Поэтому, если вы запустите приведенный выше код и не закроете окно alert
в течение некоторого времени, то следующее alert
будет показано сразу же, как вы это сделаете. Фактический интервал между оповещениями будет короче 2 секунд.
Есть два способа регулярного запуска чего-либо.
Один из них — setInterval
. Другой — вложенный setTimeout
, например:
/** вместо: let timerId = setInterval(() => alert('тик'), 2000); */ let timerId = setTimeout(функция тик() { предупреждение('галочка'); timerId = setTimeout(тик, 2000); // (*) }, 2000);
Приведенный выше setTimeout
планирует следующий вызов сразу в конце текущего (*)
.
Вложенный метод setTimeout
— более гибкий метод, чем setInterval
. Таким образом, следующий звонок может быть запланирован по-разному, в зависимости от результатов текущего.
Например, нам нужно написать сервис, который каждые 5 секунд отправляет запрос на сервер с запросом данных, но в случае перегрузки сервера он должен увеличить интервал до 10, 20, 40 секунд…
Вот псевдокод:
пусть задержка = 5000; let timerId = setTimeout (запрос функции () { ...отправить запрос... if (запрос не выполнен из-за перегрузки сервера) { // увеличиваем интервал до следующего запуска задержка *= 2; } timerId = setTimeout (запрос, задержка); }, задерживать);
А если функции, которые мы планируем, сильно нагружают процессор, то мы можем измерить время, затраченное на выполнение, и спланировать следующий вызов раньше или позже.
Вложенный setTimeout
позволяет установить задержку между выполнениями более точно, чем setInterval
.
Сравним два фрагмента кода. Первый использует setInterval
:
пусть я = 1; setInterval(функция() { функция (я++); }, 100);
Второй использует вложенный setTimeout
:
пусть я = 1; setTimeout(функция run() { функция (я++); setTimeout (запустить, 100); }, 100);
Для setInterval
внутренний планировщик будет запускать func(i++)
каждые 100 мс:
Вы заметили?
Реальная задержка между вызовами func
setInterval
меньше, чем в коде!
Это нормально, поскольку время, затраченное на выполнение func
, «съедает» часть интервала.
Вполне возможно, что выполнение func
окажется дольше, чем мы ожидали, и займет более 100 мс.
В этом случае движок ожидает завершения func
, затем проверяет планировщик и, если время истекло, немедленно запускает его снова.
В крайнем случае, если функция всегда выполняется дольше, чем delay
мс, вызовы будут происходить вообще без паузы.
А вот изображение вложенного setTimeout
:
Вложенный setTimeout
гарантирует фиксированную задержку (здесь 100 мс).
Это потому, что новый звонок планируется в конце предыдущего.
Сбор мусора и обратный вызов setInterval/setTimeout
Когда функция передается в setInterval/setTimeout
, на нее создается внутренняя ссылка и сохраняется в планировщике. Это предотвращает сбор мусора для функции, даже если на нее нет других ссылок.
// функция остается в памяти, пока ее не вызовет планировщик setTimeout(function() {...}, 100);
Для setInterval
функция остается в памяти до тех пор, пока не будет вызвана clearInterval
.
Есть побочный эффект. Функция ссылается на внешнее лексическое окружение, поэтому пока она жива, внешние переменные тоже живут. Они могут занимать гораздо больше памяти, чем сама функция. Поэтому, когда запланированная функция нам больше не нужна, лучше ее отменить, даже если она очень маленькая.
Есть особый вариант использования: setTimeout(func, 0)
или просто setTimeout(func)
.
Это планирует выполнение func
как можно скорее. Но планировщик вызовет его только после завершения текущего сценария.
Таким образом, функция запланирована на запуск «сразу после» текущего скрипта.
Например, это выводит «Hello», а затем сразу же «World»:
setTimeout(() => alert("Мир"); Оповещение("Привет");
Первая строка «помещает звонок в календарь через 0 мс». Но планировщик будет «проверять календарь» только после завершения текущего скрипта, поэтому "Hello"
идет первым, а "World"
— после него.
Существуют также расширенные варианты использования тайм-аута с нулевой задержкой, связанные с браузером, которые мы обсудим в главе «Цикл событий: микрозадачи и макрозадачи».
Нулевая задержка на самом деле не ноль (в браузере)
В браузере существует ограничение на частоту запуска вложенных таймеров. В HTML Living Standard говорится: «после пяти вложенных таймеров интервал должен составлять не менее 4 миллисекунд».
Давайте продемонстрируем, что это значит, на примере ниже. Вызов setTimeout
в нем перепланирует себя с нулевой задержкой. Каждый вызов запоминает реальное время предыдущего вызова в массиве times
. Как выглядят реальные задержки? Давайте посмотрим:
пусть старт = Date.now(); пусть раз = []; setTimeout(функция run() { times.push(Date.now() - начало); // запоминаем задержку от предыдущего вызова if (start + 100 < Date.now()) alert(times); // показываем задержки через 100 мс еще setTimeout (запустить); // иначе перепланируем }); // пример вывода: // 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
Первые таймеры запускаются сразу (как написано в спецификации), а затем мы видим 9, 15, 20, 24...
. В игру вступает обязательная задержка 4+ мс между вызовами.
То же самое происходит, если мы используем setInterval
вместо setTimeout
: setInterval(f)
запускает f
несколько раз с нулевой задержкой, а затем с задержкой 4+ мс.
Это ограничение пришло из древних времен, и на него опираются многие сценарии, поэтому оно существует по историческим причинам.
Для серверного JavaScript такого ограничения не существует, и существуют другие способы запланировать немедленное асинхронное задание, например setImmediate для Node.js. Итак, это примечание зависит от браузера.
Методы setTimeout(func, delay, ...args)
и setInterval(func, delay, ...args)
позволяют нам запускать func
один раз/регулярно после delay
в миллисекундах.
Чтобы отменить выполнение, мы должны clearTimeout/clearInterval
со значением, возвращаемым setTimeout/setInterval
.
Вложенные вызовы setTimeout
— более гибкая альтернатива setInterval
, позволяющая нам более точно устанавливать время между выполнениями.
Планирование нулевой задержки с помощью setTimeout(func, 0)
(то же самое, что и setTimeout(func)
) используется для планирования вызова «как можно скорее, но после завершения текущего сценария».
Браузер ограничивает минимальную задержку для пяти или более вложенных вызовов setTimeout
или setInterval
(после 5-го вызова) до 4 мс. Это по историческим причинам.
Обратите внимание, что все методы планирования не гарантируют точную задержку.
Например, таймер в браузере может замедляться по множеству причин:
Процессор перегружен.
Вкладка браузера находится в фоновом режиме.
Ноутбук находится в режиме экономии заряда батареи.
Все это может увеличить минимальное разрешение таймера (минимальную задержку) до 300 мс или даже 1000 мс в зависимости от настроек производительности на уровне браузера и ОС.
важность: 5
Напишите функцию printNumbers(from, to)
, которая выводит число каждую секунду, начиная с from
и заканчивая to
.
Придумайте два варианта решения.
Использование setInterval
.
Использование вложенного setTimeout
.
Использование setInterval
:
функция printNumbers(от, до) { пусть текущий = от; let timerId = setInterval(function() { оповещение (текущий); если (текущий == до) { ClearInterval (timerId); } текущий++; }, 1000); } // использование: printNumbers(5, 10);
Использование вложенного setTimeout
:
функция printNumbers(от, до) { пусть текущий = от; setTimeout (функция go () { оповещение (текущий); если (текущий <до) { setTimeout (идти, 1000); } текущий++; }, 1000); } // использование: printNumbers(5, 10);
Обратите внимание, что в обоих решениях перед первым выводом имеется начальная задержка. Функция вызывается через 1000ms
в первый раз.
Если мы также хотим, чтобы функция запускалась немедленно, мы можем добавить дополнительный вызов в отдельной строке, например:
функция printNumbers(от, до) { пусть текущий = от; функция идти() { оповещение (текущий); если (текущий == до) { ClearInterval (timerId); } текущий++; } идти(); пусть timerId = setInterval (go, 1000); } printNumbers(5, 10);
важность: 5
В приведенном ниже коде запланирован вызов setTimeout
, затем выполняются тяжелые вычисления, для завершения которых требуется более 100 мс.
Когда будет запущена запланированная функция?
После цикла.
Перед циклом.
В начале цикла.
Что покажет alert
?
пусть я = 0; setTimeout(() => alert(i), 100); // ? // предположим, что время выполнения этой функции >100 мс for(let j = 0; j < 100000000; j++) { я++; }
Любой setTimeout
будет запущен только после завершения текущего кода.
i
будет последним: 100000000
.
пусть я = 0; setTimeout(() => alert(i), 100); // 100000000 // предположим, что время выполнения этой функции >100 мс for(let j = 0; j < 100000000; j++) { я++; }