Цикл событий
Некоторые действия занимают время: ожидание таймера, ответ сервера, загрузка файла. JavaScript не стоит на месте, пока они идут, а продолжает работу и возвращается к результату позже. Чтобы писать такой код без сюрпризов, нужно понимать, в каком порядке он выполняется.
Синхронный код
Обычный код называют синхронным: инструкции выполняются строго по порядку, следующая начинается только после окончания предыдущей:
console.log('Один');
console.log('Два');
console.log('Три');
// Один
// Два
// ТриJavaScript однопоточный: в каждый момент он выполняет только одну инструкцию. Две функции не могут работать одновременно.
Асинхронный код
Асинхронный код — это код, который запускается сейчас, а заканчивается позже. Функция setTimeout() откладывает вызов другой функции на заданное число миллисекунд:
setTimeout(() => {
console.log('Прошло 300 мс');
}, 300);
// Прошло 300 мсПока таймер ждёт, JavaScript свободен и может выполнять другой код.
Очередь задач
Когда таймер срабатывает, его функция не выполняется сразу, а встаёт в очередь задач. JavaScript берёт задачи из очереди, только когда закончит текущий код:
console.log('Начало');
setTimeout(() => {
console.log('Таймер');
}, 0);
console.log('Конец');
// Начало
// Конец
// ТаймерДаже с нулевой задержкой таймер выполнился последним: сначала закончился весь основной код.
Цикл событий
Механизм, который снова и снова проверяет очередь и запускает следующую задачу, называется циклом событий (event loop). Его работу можно описать так:
| Шаг | Что происходит |
|---|---|
| 1 | Выполняется весь текущий синхронный код |
| 2 | Выполняются все микрозадачи — обработчики промисов |
| 3 | Из очереди берётся одна задача, например функция таймера |
| 4 | Снова шаг 2 — и так, пока есть задачи |
Микрозадачи
Обработчики промисов (о них — в следующих уроках) попадают в отдельную очередь микрозадач. Она выполняется раньше обычных задач:
setTimeout(() => console.log('Таймер'), 0);
Promise.resolve().then(() => console.log('Промис'));
console.log('Обычный код');
// Обычный код
// Промис
// ТаймерДолгий код всё задерживает
Пока выполняется синхронный код, ни одна задача из очереди не запустится. Этот цикл занимает JavaScript на 200 мс, и таймер ждёт его окончания:
const start = Date.now();
setTimeout(() => {
const waited = Date.now() - start;
console.log(waited >= 200); // true
}, 0);
while (Date.now() - start < 200) {
// просто ждём
}В браузере на это время замирает вся страница: кнопки не нажимаются, анимация останавливается.
Частая ошибка
Ожидать, что setTimeout(..., 0) выполнится немедленно. Задержка — это лишь минимальное время ожидания: функция запустится не раньше, чем закончится текущий код и все микрозадачи. Если результат нужен сразу, не откладывайте код в таймер, а если нужен порядок «сначала одно, потом другое» — используйте промисы и await.