Событийный цикл и итерация
Однопоточность JavaScript кажется парадоксом: как один поток обслуживает таймеры, сеть, клики и промисы, ничего не замораживая? Ответ — событийный цикл Event Loop: механизм, который забирает готовые колбэки из очередей и по одному подаёт их на пустой стек. Понимание порядка этих очередей — микрозадачи против макрозадач — отличает того, кто предсказывает вывод программы, от того, кто гадает.
Вторая половина темы — протокол итерации: единый интерфейс [Symbol.iterator], на котором держатся for...of, spread и деструктуризация, и генераторы — функции, умеющие приостанавливаться и возобновляться. Они же концептуально лежат под async/await. Завершают тему реактивные push-потоки как альтернативная модель работы со временем.
Карта темы
- Event Loop — один стек, очередь задач и правило «выполнять задачу только после опустошения стека».
- Микро- и макрозадачи — почему промисы обгоняют таймеры и как полное опустошение очереди микрозадач морит макрозадачи.
- Итераторы и итерируемые — протокол
[Symbol.iterator]иnext(), на котором работаютfor...ofи spread. - Генераторы —
function*с приостановкой наyieldи ленивым созданием значений по запросу. - Реактивные потоки — RxJS, горячие и холодные observable, backpressure и pull против push.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать event loop многопоточным | Ложная модель — он строго однопоточный, колбэки идут по одному |
Думать, что setTimeout(fn, 0) срабатывает раньше Promise.then | Микрозадачи всегда опустошаются до следующей макрозадачи |
| Полагать, что цикл берёт по одной микрозадаче на макрозадачу | Он опустошает всю очередь микрозадач между макрозадачами |
Перебирать обычный объект через for...of | У него нет [Symbol.iterator] — будет ошибка |
| Думать, что вызов генератора выполняет его тело | Он лишь возвращает приостановленный итератор |
Забыть unsubscribe у горячего observable | Утечка подписки и её источника |
Значение для собеседований
Порядок event loop — классический senior-вопрос «предскажите вывод». Интервьюер даёт кусок с setTimeout, Promise.then и синхронными console.log и смотрит, разложите ли вы его по фазам. Отдельно спрашивают, что делает объект итерируемым и как генератор моделирует бесконечную последовательность без бесконечной памяти.
Что обычно проверяют:
- Что поток один, а колбэки выполняются по одному до конца (run-to-completion).
- Что вся очередь микрозадач опустошается до следующей макрозадачи.
- Протокол итерации и разницу
for...of(значения) иfor...in(ключи). - Что вызов генератора возвращает итератор, а значения создаются лениво.
Типичный неверный ответ: «setTimeout(fn, 0) выполнится сразу и раньше промисов». На деле колбэк таймера — макрозадача: он ждёт и опустошения стека, и полного слива очереди микрозадач, поэтому любой Promise.then его обгоняет.