Как обрабатываются call stack и очереди в JavaScript
Что проверяют
Понимаете ли вы, что язык однопоточный, и знаете ли, чем стек вызовов отличается от очередей задач.
Как отвечать
Стек вызовов исполняет синхронный код: вызов функции кладёт кадр наверх, возврат снимает. Пока в стеке что-то есть, ничего другого не выполняется, включая отрисовку и обработку кликов. Отсюда зависание вкладки при тяжёлом вычислении.
Асинхронность работает через очереди. Когда операция завершается, её колбэк попадает в одну из очередей, и цикл событий берёт оттуда задачу, только когда стек пуст:
- очередь микрозадач: колбэки промисов,
queueMicrotask,MutationObserver. Разгребается целиком, включая микрозадачи, порождённые во время разгребания; - очередь макрозадач: таймеры, события ввода, сетевые колбэки. За один оборот берётся одна задача, после чего снова разгребаются микрозадачи.
Отсюда порядок: синхронный код, потом микрозадачи, потом одна макрозадача, и цикл повторяется. Между оборотами браузер может отрисовать кадр.
Полезно назвать практические следствия: переполнение стека при глубокой рекурсии (RangeError), причём асинхронный вызов стек не наращивает, потому что колбэк начинается с чистого стека; бесконечная цепочка микрозадач блокирует рендер намертво; тяжёлый расчёт дробят на части или уносят в Worker, у которого свой поток и свой стек.
Стоит упомянуть и разницу с Node.js: там цикл разделён на фазы, setImmediate относится к фазе проверки, а process.nextTick выполняется раньше промисов.
Где спотыкаются
Считают, что асинхронный код выполняется параллельно.
Не понимают, почему интерфейс не отвечает во время долгого цикла, ведь код «асинхронный».
Путают порядок микро- и макрозадач и не могут объяснить вывод классического примера с таймером и промисом.