Таймеры и асинхронность
Зачем это нужно
До сих пор программы выполнялись строго сверху вниз: строка за строкой, без пауз. Но настоящим программам постоянно нужно ждать: показать подсказку через секунду, обновлять часы каждую секунду, дождаться ответа сервера, отреагировать на клик. Код, который выполняется не сразу, а потом, называют асинхронным.
Таймеры — самый простой асинхронный код. На них хорошо видно главное правило JavaScript:
отложенный код никогда не прерывает текущий, а ждёт своей очереди. Без понимания этого правила
не понять ни промисы, ни async/await, ни то, когда React обновляет страницу.
setTimeout — выполнить один раз позже
setTimeout(() => {
console.log("Прошла секунда");
}, 1000);
console.log("Таймер поставлен");
Вывод:
Таймер поставлен
Прошла секунда
- Первый аргумент — функция (колбэк), которую нужно вызвать.
- Второй — задержка в миллисекундах: 1000 мс = 1 секунда.
setTimeoutне ждёт: он записывает «вызвать через секунду» и сразу возвращает управление. Поэтому «Таймер поставлен» печатается первым.
Колбэку можно передать данные через замыкание:
function remind(text, ms) {
setTimeout(() => console.log("Напоминание: " + text), ms);
}
remind("попить воды", 200);
remind("размяться", 100);
Вывод:
Напоминание: размяться
Напоминание: попить воды
Срабатывают в порядке времени, а не в порядке вызова.
Отменить: clearTimeout
setTimeout возвращает номер таймера. По нему таймер можно отменить, пока он не сработал:
const id = setTimeout(() => console.log("Бум!"), 500);
clearTimeout(id);
console.log("Таймер отменён");
Вывод:
Таймер отменён
«Бум!» не напечатается никогда.
setInterval — повторять
setInterval(колбэк, мс) вызывает функцию снова и снова с паузой мс, пока его не остановят
через clearInterval:
let count = 0;
const id = setInterval(() => {
count++;
console.log("Тик " + count);
if (count === 3) {
clearInterval(id);
console.log("Стоп");
}
}, 100);
Вывод:
Тик 1
Тик 2
Тик 3
Стоп
Забытый clearInterval — частая ошибка: интервал будет работать вечно.
Почему отложенный код ждёт: цикл событий
JavaScript выполняет код в одном потоке: в каждый момент времени работает только одна функция. Пока она не закончилась, ничто другое не запустится. Устроено это так:
- Сначала выполняется весь обычный (синхронный) код — сверху вниз.
- Готовые к запуску колбэки (сработавший таймер, клик, ответ сервера) встают в очередь.
- Когда текущий код закончился, цикл событий (event loop) берёт из очереди следующий колбэк и выполняет его целиком. Потом следующий — и так всё время работы программы.
Поэтому даже таймер с задержкой 0 не срабатывает сразу:
console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");
Вывод:
1
3
2
setTimeout(…, 0) значит «как можно скорее, но после текущего кода». А задержка — это
минимальное время ожидания: если программа занята долгим циклом, таймер подождёт, пока она
освободится.
const start = Date.now();
setTimeout(() => console.log("Таймер опоздал: " + (Date.now() - start >= 100)), 10);
while (Date.now() - start < 100) {
// занятый цикл на 100 мс — очередь ждёт
}
console.log("Цикл закончился");
Вывод:
Цикл закончился
Таймер опоздал: true
Таймер был готов через 10 мс, но выполнился только после того, как цикл освободил поток. Поэтому в браузере тяжёлые вычисления «замораживают» страницу: клики тоже ждут в очереди.
Колбэки и «ад колбэков»
Асинхронная функция не может просто вернуть результат через return — результата ещё нет. Поэтому
её просят: «когда будет готово, вызови вот эту функцию»:
function loadUser(id, callback) {
setTimeout(() => callback({ id, name: "Аня" }), 50); // как будто ответ сервера
}
loadUser(1, (user) => console.log("Загружен: " + user.name));
console.log("Загружаем...");
Вывод:
Загружаем...
Загружен: Аня
Если шагов несколько и каждый ждёт предыдущий, колбэки вкладываются друг в друга:
loadUser(1, (user) => {
loadOrders(user, (orders) => {
loadDetails(orders[0], (details) => {
// ...и ещё глубже
});
});
});
Такую «лесенку» называют адом колбэков (callback hell): её трудно читать, а ошибки приходится обрабатывать на каждом уровне. Эту проблему решают промисы — следующая тема.
Debounce: ждать, пока пользователь остановится
Полезный приём на таймерах. Пользователь печатает в поиске, и на каждую букву отправлять запрос на сервер расточительно. Лучше подождать, пока он перестанет печатать:
function debounce(fn, ms) {
let timer; // живёт в замыкании между вызовами
return (...args) => {
clearTimeout(timer); // новый вызов отменяет старый таймер
timer = setTimeout(() => fn(...args), ms);
};
}
const search = debounce((q) => console.log("Ищем: " + q), 100);
search("j");
search("ja");
search("jav");
Вывод:
Ищем: jav
Три быстрых вызова — один запрос, с последним значением.
Как это работает внутри
Сами таймеры считает не JavaScript, а окружение: браузер или Node.js. Когда вызывается
setTimeout, окружение запоминает функцию и время. Когда время подходит, оно кладёт колбэк в
очередь задач. Движок JavaScript выполняет синхронный код до конца, а потом цикл событий
достаёт задачи из очереди по одной.
Отсюда три следствия:
- колбэк таймера никогда не выполнится посреди другой функции;
- задержка — это «не раньше чем», а не «ровно через»;
- пока колбэков нет, программа просто ждёт. В Node.js программа завершается, когда очередь пуста и не осталось ни одного таймера.
Частые ошибки
- Вызов вместо передачи.
setTimeout(sayHi(), 1000)вызываетsayHiсразу и передаёт таймеру её результат. НужноsetTimeout(sayHi, 1000)илиsetTimeout(() => sayHi(), 1000). - Ждать результат через
return. Значение, полученное внутри колбэка таймера, нельзя вернуть из внешней функции: она уже завершилась. Нужен колбэк (или промис). - Забыли
clearInterval. Интервал работает, пока его не остановят. - Секунды вместо миллисекунд.
setTimeout(fn, 5)— это 5 мс, а не 5 секунд. - Думать, что
0— это «сразу». Колбэк выполнится только после всего текущего кода.
Шпаргалка
| Что | Как |
|---|---|
| Выполнить один раз позже | const id = setTimeout(fn, ms) |
| Отменить | clearTimeout(id) |
| Повторять | const id = setInterval(fn, ms) |
| Остановить повтор | clearInterval(id) |
| После текущего кода | setTimeout(fn, 0) |
| Порядок | сначала весь синхронный код, потом очередь |
| Debounce | clearTimeout(timer); timer = setTimeout(...) |