JavaScript setTimeout и setInterval
Узнайте, как планировать выполнение кода в JavaScript с помощью setTimeout и setInterval: синтаксис, передача аргументов, отмена таймеров, рекурсивный setTimeout, нулевая задержка, debouncing и throttling.
Иногда не нужно выполнять код прямо сейчас — нужно запустить его позже или запускать многократно. Две функции планирования JavaScript, setTimeout() и setInterval(), позволяют сделать именно это. В этом руководстве рассматриваются их синтаксис, способы передачи аргументов, отмена запланированного таймера, важный паттерн «рекурсивный setTimeout», неочевидное поведение нулевой задержки, а также два практических применения: debouncing и throttling.
Ни одна из этих функций не является частью базового языка JavaScript — они предоставляются средой выполнения (браузерами и Node.js). Описанное здесь поведение одинаково в обоих случаях, с несколькими отмеченными различиями.
Введение в функции таймингов JavaScript
Таймер планирует выполнение колбэка после того, как завершится текущий код и пройдёт указанное время. Поскольку JavaScript однопоточный, колбэк никогда не прерывает выполняющийся код — он ожидает в очереди и запускается только тогда, когда стек вызовов пуст. (Подробнее о механизме см. в разделе Событийный цикл.)
Функция setTimeout()
setTimeout() выполняет функцию один раз после указанной задержки. Принимает функцию для выполнения и задержку в миллисекундах перед этим выполнением.
Синтаксис
let timerId = setTimeout(func, delay, arg1, arg2, ...);func— функция (или, реже, строка кода) для выполнения.delay— время ожидания в миллисекундах перед запуском. По умолчанию равно0.arg1, arg2, ...— необязательные аргументы, которые передаются непосредственно вfunc.
Возвращаемое значение — числовой идентификатор таймера, который позже можно передать в clearTimeout().
Пример
Передача аргументов в колбэк
Всё, что указано после задержки, передаётся в колбэк. Это чище, чем оборачивать вызов в другую стрелочную функцию:
setTimeout(greet(), 1000) выполняет greet() немедленно и планирует её возвращаемое значение (скорее всего undefined). Пишите setTimeout(greet, 1000) — без скобок.Функция setInterval()
setInterval() имеет ту же сигнатуру, что и setTimeout(), но вместо однократного запуска колбэка выполняет его многократно каждые delay миллисекунд, пока вы его не остановите.
Синтаксис
let timerId = setInterval(func, delay, arg1, arg2, ...);Пример
setTimeout, описанный ниже.Рекурсивный setTimeout против setInterval
Поведение setInterval() можно воспроизвести, заставив колбэк setTimeout() перепланировать сам себя. Ключевое различие: setInterval() измеряет задержку между началами вызовов, тогда как рекурсивный setTimeout() измеряет её между окончанием одного выполнения и началом следующего — гарантируя фиксированную паузу даже при медленном колбэке.
Отмена запланированного выполнения
Обе функции возвращают идентификатор таймера. Сохранив этот идентификатор, можно отменить запланированную работу с помощью clearTimeout() или clearInterval(). (Обе функции очистки фактически взаимозаменяемы в большинстве движков, но сопоставление их с соответствующим планировщиком делает код читаемее.)
Остановка setTimeout()
Чтобы отменить таймаут, сохраните идентификатор, возвращённый setTimeout(), и передайте его в clearTimeout() до истечения задержки.
Пример
Остановка setInterval()
Аналогично, сохраните идентификатор из setInterval() и передайте его в clearInterval(). Без этого интервал будет выполняться бесконечно (или до закрытия страницы), что является распространённой причиной утечек памяти и бесконтрольных таймеров.
Пример
setTimeout с нулевой задержкой
setTimeout(func, 0) не выполняет func немедленно. Он планирует выполнение func сразу после того, как текущий синхронный код завершится. Это удобный способ «уступить управление» — дать браузеру перерисовать страницу или разбить длинную задачу на части — и он объясняет порядок вывода, который часто удивляет новичков:
Обратите внимание, что таймеры — это макрозадачи: они выполняются после всех поставленных в очередь микрозадач (например, колбэков выполненных промисов). Точные правила порядка описаны в разделе Событийный цикл: микрозадачи и макрозадачи.
Практическое применение и советы
Два наиболее распространённых реальных применения setTimeout() — это debouncing и throttling — оба являются способами ограничить частоту вызова функции в ответ на быстро следующие друг за другом события.
Debouncing с помощью setTimeout()
Debouncing ждёт, пока серия событий не прекратится, и только затем выполняет функцию. Каждое новое событие сбрасывает таймер, поэтому колбэк срабатывает только после того, как активность стихает на wait миллисекунд. Это идеально подходит для поля поиска: нужно отправить один запрос после того, как пользователь закончил печатать, а не на каждое нажатие клавиши.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Debounced Input Example</title>
<script>
// Debounce function to limit the rate at which a function is executed
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Function to be debounced
function fetchData(input) {
alert(`API call with input: ${input}`); // Placeholder for an API call
}
// Create a debounced version of fetchData
const debouncedFetchData = debounce(fetchData, 300);
// Add the debounced function to an event listener
function setup() {
document.getElementById('searchInput').addEventListener('input', (event) => {
debouncedFetchData(event.target.value);
});
}
// Ensure setup is called once the document is fully loaded
document.addEventListener('DOMContentLoaded', setup);
</script>
</head>
<body>
<h3>Type in the input field:</h3>
<input type="text" id="searchInput" placeholder="Start typing..." />
</body>
</html>Throttling с помощью setTimeout()
Throttling выполняет функцию не чаще одного раза в limit миллисекунд, независимо от количества событий между ними. Если debouncing ждёт тишины, throttling гарантирует равномерный ритм — идеально для обработчиков scroll, resize или mousemove, которые иначе срабатывали бы десятки раз в секунду. Пример ниже использует подход leading-edge (выполняется сразу при первом событии, затем соблюдает паузу):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Throttled Scroll Event</title>
<style>
/* Simple styling for demonstration */
body, html {
height: 200%; /* Make the page scrollable */
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
#log {
position: fixed;
top: 0;
left: 0;
background: white;
border: 1px solid #ccc;
padding: 10px;
width: 300px;
}
</style>
</head>
<body>
<div id="log">Scroll to see the effect...</div>
<script>
// Throttle function using setTimeout
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, Math.max(0, limit - (Date.now() - lastRan)));
}
}
}
// Function to be throttled
function handleScroll() {
const log = document.getElementById('log');
log.textContent = `Scroll event triggered at: ${new Date().toLocaleTimeString()}`;
}
// Add event listener for scroll
window.addEventListener('scroll', throttle(handleScroll, 1000));
</script>
</body>
</html>Важные нюансы
- Задержки — это минимум, а не гарантия. Если стек вызовов занят или событийный цикл перегружен, колбэк ждёт. Число, которое вы передаёте, — это самое раннее время запуска, а не обещание.
- Фоновые вкладки имеют ограничения. Большинство браузеров замедляют таймеры в неактивных вкладках примерно до одного раза в секунду для экономии энергии, поэтому анимации и опросы замедляются, когда вкладка скрыта.
- Вложенные таймауты ограничены ~4мс. После пяти вложенных вызовов
setTimeout()браузеры устанавливают минимальную задержку около 4 миллисекунд, поэтому задержка0никогда не является по-настоящему нулевой в глубоких цепочках. - Максимальная задержка. Задержка, превышающая
2147483647(2^31 − 1), вызывает переполнение 32-битного поля и воспринимается как0, что приводит к немедленному срабатыванию вместо запуска в далёком будущем. - Привязка
this. При передаче метода в видеsetTimeout(obj.method, 1000)теряетсяthis. Используйте стрелочную функцию —setTimeout(() => obj.method(), 1000)— илиobj.method.bind(obj). - Всегда выполняйте очистку. Очищайте интервалы (и ожидающие таймауты), когда компонент размонтируется или работа больше не нужна, иначе произойдут утечки таймеров и вы можете работать с устаревшим состоянием.
Связанные темы
- Введение: колбэки — основа, на которой строятся таймеры.
- Событийный цикл: микрозадачи и макрозадачи — почему таймаут в
0мс всё равно выполняется последним. - Promise и Async/await — современные альтернативы для последовательного выполнения асинхронной работы.
- Рекурсия и стек — основа для паттерна рекурсивного
setTimeout.
Заключение
setTimeout() выполняет код один раз после задержки; setInterval() запускает его по повторяющемуся расписанию; clearTimeout() и clearInterval() отменяют их. Помните, что задержки — это минимумы, передавайте аргументы после задержки, а не внутри обёртки, используйте рекурсивный паттерн setTimeout, когда нужна равномерная периодичность, и всегда очищайте таймеры, которые больше не нужны. Добавив debouncing и throttling, эти две небольшие функции покрывают большую часть временных задач в браузере.