setTimeout и setInterval
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Иногда коду нужно подождать: показать подсказку через три секунды, обновлять счётчик каждую секунду, отложить тяжёлую работу до конца текущей. Для этого у браузера есть два таймера — setTimeout и setInterval, и оба возвращают идентификатор, по которому вызов можно отменить.
setTimeout берёт функцию и число миллисекунд и говорит браузеру: «вызови её потом». Программа на этом не останавливается — строки ниже выполнятся сразу, а колбэк дождётся своей очереди.
Вызов возвращает идентификатор таймера — в браузере это число. Пока время не вышло, по идентификатору таймер можно отменить: clearTimeout(id). Отменять уже сработавший или несуществующий таймер безопасно — просто ничего не произойдёт.
Если колбэку нужны данные, их перечисляют после задержки — третьим, четвёртым и любым следующим аргументом. Вариант с обёрткой setTimeout(() => sum(4, 5), 500) тоже рабочий и часто читается понятнее.
А вот скобки после имени функции — классическая ловушка. setTimeout(ping, 500) передаёт саму функцию, а setTimeout(ping(), 500) сначала вызывает ping прямо сейчас и отдаёт таймеру то, что она вернула, — обычно undefined. Таймер честно отсчитает полсекунды и ничего не сделает.
Метод, оторванный от объекта, теряет this: таймер вызывает его как обычную функцию. Спасает стрелочная обёртка — она вызывает метод через объект, как обычный код.
setInterval устроен точно так же — те же аргументы, тот же идентификатор, — но повторяет вызов, пока его не остановят через clearInterval. Забыть про остановку легко, и тогда таймер будет тикать до перезагрузки страницы.
Задержка — это не «через 300 мс», а «не раньше чем через 300 мс». Очередь может быть занята.
Есть тонкость, из-за которой интервалы часто ведут себя не так, как ждут: пауза отсчитывается от старта предыдущего вызова, а не от его конца. Если колбэк работает 900 мс при интервале в 1000, на отдых остаётся сотня. А если он затянется дольше интервала, вызовы пойдут вплотную, друг за другом.
Ту же периодичность можно собрать из одиночных таймеров: колбэк в конце своей работы планирует сам себя. Тогда пауза отсчитывается от конца предыдущего вызова и всегда получается полной — сколько бы ни длилась работа внутри.
Бонусом задержку можно менять на ходу: перед следующим планированием подставьте другое число. Так делают опрос сервера с нарастающей паузой — 100 мс, 200, 400, 800, — чтобы не бомбить его запросами, пока он лежит.
| setInterval | вложенный setTimeout | |
|---|---|---|
| Пауза считается | от старта предыдущего вызова | от конца предыдущего вызова |
| Колбэк работает долго | пауза съедается, вызовы слипаются | пауза остаётся полной |
| Смена задержки | нужно пересоздавать таймер | новое число при каждом планировании |
| Остановка | clearInterval(id) | clearTimeout последнего id |
setTimeout(f, 0) не выполняет f немедленно. Это значит «выполни, как только освободится очередь», то есть после всего синхронного кода. Приём удобен, когда нужно дать браузеру вдохнуть: дорисовать страницу, отреагировать на клик — и уже потом продолжить.
И самого нуля браузер не обещает. По стандарту с седьмого таймера в цепочке вложенных друг в друга задержка зажимается минимум до 4 мс, а в неактивной вкладке задержка вырастает до секунды и больше. Отсчитывать время таймерами не стоит — для этого есть Date.now() и performance.now().
Пока вкладка не на виду, браузер экономит батарею и будит таймеры всё реже: первую минуту секундный таймер ещё попадает в срок, а дальше промежуток между пробуждениями вырастает до десятков секунд. Замер в 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, а затем строку «Поехали!» — и на этом таймер должен полностью остановиться.
Подсказка: идентификатор таймера нужен внутри самого колбэка, поэтому сохраните его в переменную до того, как счётчик дойдёт до нуля.
Считаем в переменной снаружи колбэка и гасим таймер, как только дошли до нуля, — иначе интервал будет крутиться вечно и уйдёт в минус.
Тот же отсчёт на вложенном setTimeout надёжнее, если внутри появится долгая работа: пауза между выводами останется ровно секундой.
Переписать интервал на вложенный setTimeout
Есть повторяющаяся задача, которая печатает «шаг 1», «шаг 2», «шаг 3» и внутри может работать долго. Сделайте так, чтобы между концом одного шага и началом следующего проходила ровно секунда, а после третьего шага повторы прекратились.
Вместо одного setInterval — цепочка одиночных таймеров: функция в конце работы планирует сама себя, и отсчёт секунды начинается уже после того, как весь код шага отработал.
Остановка тут бесплатная: не запланировали следующий вызов — цепочка закончилась. Отменять что-то через clearTimeout нужно, только если прервать её хотят снаружи.