setTimeout и setInterval

асинхронность≈ 7 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

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

Разделы темы:·

setTimeout берёт функцию и число миллисекунд и говорит браузеру: «вызови её потом». Программа на этом не останавливается — строки ниже выполнятся сразу, а колбэк дождётся своей очереди.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Вызов возвращает идентификатор таймера — в браузере это число. Пока время не вышло, по идентификатору таймер можно отменить: clearTimeout(id). Отменять уже сработавший или несуществующий таймер безопасно — просто ничего не произойдёт.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Если колбэку нужны данные, их перечисляют после задержки — третьим, четвёртым и любым следующим аргументом. Вариант с обёрткой setTimeout(() => sum(4, 5), 500) тоже рабочий и часто читается понятнее.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

А вот скобки после имени функции — классическая ловушка. setTimeout(ping, 500) передаёт саму функцию, а setTimeout(ping(), 500) сначала вызывает ping прямо сейчас и отдаёт таймеру то, что она вернула, — обычно undefined. Таймер честно отсчитает полсекунды и ничего не сделает.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Метод, оторванный от объекта, теряет this: таймер вызывает его как обычную функцию. Спасает стрелочная обёртка — она вызывает метод через объект, как обычный код.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Задержка — это не «через 300 мс», а «не раньше чем через 300 мс». Очередь может быть занята.

Есть тонкость, из-за которой интервалы часто ведут себя не так, как ждут: пауза отсчитывается от старта предыдущего вызова, а не от его конца. Если колбэк работает 900 мс при интервале в 1000, на отдых остаётся сотня. А если он затянется дольше интервала, вызовы пойдут вплотную, друг за другом.

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

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Бонусом задержку можно менять на ходу: перед следующим планированием подставьте другое число. Так делают опрос сервера с нарастающей паузой — 100 мс, 200, 400, 800, — чтобы не бомбить его запросами, пока он лежит.

setIntervalвложенный setTimeout
Пауза считаетсяот старта предыдущего вызоваот конца предыдущего вызова
Колбэк работает долгопауза съедается, вызовы слипаютсяпауза остаётся полной
Смена задержкинужно пересоздавать таймерновое число при каждом планировании
ОстановкаclearInterval(id)clearTimeout последнего id

setTimeout(f, 0) не выполняет f немедленно. Это значит «выполни, как только освободится очередь», то есть после всего синхронного кода. Приём удобен, когда нужно дать браузеру вдохнуть: дорисовать страницу, отреагировать на клик — и уже потом продолжить.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

И самого нуля браузер не обещает. По стандарту с седьмого таймера в цепочке вложенных друг в друга задержка зажимается минимум до 4 мс, а в неактивной вкладке задержка вырастает до секунды и больше. Отсчитывать время таймерами не стоит — для этого есть Date.now() и performance.now().

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Пока вкладка не на виду, браузер экономит батарею и будит таймеры всё реже: первую минуту секундный таймер ещё попадает в срок, а дальше промежуток между пробуждениями вырастает до десятков секунд. Замер в Chrome 154: секундный setInterval в скрытой вкладке за 6,5 минуты успел 68 тиков вместо 395 — счётчик, который просто прибавляет единицу, отстал больше чем на пять минут.

Лечится это не таймером, а арифметикой: запомните момент старта через Date.now() и на каждом тике считайте разницу. Тогда пропущенные тики ничего не портят — цифра всё равно верная.

  • setTimeout(f, задержка) выполняет функцию один раз и возвращает идентификатор; clearTimeout(id) отменяет вызов, пока время не вышло.
  • Аргументы для колбэка идут после задержки: setTimeout(sum, 500, 4, 5). Скобки после имени функции вызывают её немедленно, и таймеру достаётся undefined.
  • setInterval повторяет вызов до clearInterval, но отсчитывает паузу от старта предыдущего вызова — при долгом колбэке отдых съедается.
  • Вложенный setTimeout, который планирует сам себя, даёт полную паузу между вызовами и позволяет менять задержку на лету.
  • Задержка означает «не раньше чем»: вложенные таймеры зажимаются до 4 мс, фоновая вкладка тормозит их до секунды, а setTimeout(f, 0) просто откладывает f до конца синхронного кода.

1/3Строка setTimeout(() => console.log("A"), 0) стоит выше, чем console.log("B"). Что выведется первым?

2/3Функция say печатает строку. Что произойдёт при вызове setTimeout(say(), 1000)?

3/3Колбэк внутри setInterval(f, 1000) иногда работает 1500 мс. Что будет с паузами?

Обратный отсчёт

Напишите обратный отсчёт: раз в секунду выводите 3, 2, 1, а затем строку «Поехали!» — и на этом таймер должен полностью остановиться.

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

Считаем в переменной снаружи колбэка и гасим таймер, как только дошли до нуля, — иначе интервал будет крутиться вечно и уйдёт в минус.

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

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

Переписать интервал на вложенный setTimeout

Есть повторяющаяся задача, которая печатает «шаг 1», «шаг 2», «шаг 3» и внутри может работать долго. Сделайте так, чтобы между концом одного шага и началом следующего проходила ровно секунда, а после третьего шага повторы прекратились.

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

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Остановка тут бесплатная: не запланировали следующий вызов — цепочка закончилась. Отменять что-то через clearTimeout нужно, только если прервать её хотят снаружи.