Асинхронность, микрозадачи и Event loop в одном вопросе

Асинхронность, микрозадачи и Event loop в одном вопросе

Maksym Pohribniak

Этот популярный вопрос на собеседованиях по JavaScript встречается в разных вариациях, но суть примерно одинакова.

Прочитайте следующий пример кода и скажите в каком порядке произойдет вывод сообщений.

Вероятно вы знаете, или слышали что-то про v8, однопоточность, коллбеки и промисы. Но уверенны ли вы что знаете как на самом деле как выполняется JavaScript?

Вывод в консоли будет следующим:

script start // строка 1
end script // строка 7
promise then // строка 5
timeout // строка 3

Проверить это можно по ссылке.

Чтобы ответить верно на данный вопрос и вопросы похожего типа нам нужно
разобраться во всех тех терминах описанных в заголовке.

Начнем с того, почему "script start" и "end script" выведены в первую очередь?

С первой строкой все ясно — так, как скрипт начинается с этого кода и интерпретируется сверху вниз — логично что первым он и исполнится. Но почему после него следует "end script"?

Все дело в том что в коде выше представлены синхронные и асинхронные операции.

Большее количество кода которое пишет JS разработчик, является синхронным, то есть — подвешивает весь процесс или команду до тех пор, пока не выполнится, и лишь затем передает управление дальше, нижестоящему коду. Например мы всегда уверенны в том, что в следующем коде:

Переменная b будет вмещать значение 10 так как выше мы определили переменную а и умножили её на число 2.

В свою очередь асинхронный код это операции требующие от системы обратиться к внешнему устройству API например setTimeout, AJAX запросы, DOM events. Или же Promise, async/await и генераторы являющиеся частью языка начиная с ES6/ES7. Пример асинхронного кода:

AJAX запрос выполнен через встроенный в браузер XMLHttpRequest.
Если мы используем его асинхронную версию, то не сможем получить доступ к полученным данным сразу же после выполнения метода .send()

Асинхронный код передает управление дальше, но точка остановки запоминается и управление к ней возвращается в будущем при каком-то условии. Например при подписке на событие "onload" или "onreadystatechange".

Главным отличием синхронных от асинхронных операций:

  1. Пока выполняется синхронный код — никакой асинхронный выполнятся не может. В нашем примере выполняется сначала весь синхронный код (console.log-и), а только потом асинхронный (Promise.then и setTimeout).
  2. Синхронный код является блокирующим. Пока происходит синхронная операция страница "подвисает" не реагирует на любые события. Например при использовании цикла на миллион итераций вы не сможете взаимодействовать со страницей какое-то время. Именно по этому тяжелые операции типа обработки изображений, операций с файлами, создание запросов сети должны быть асинхронными. Ведь мы можем исполнять другой полезный код пока ждем коллбек или промис.

И тут появляется Event Loop (или цикл обработки событий).

JavaScript код в браузере выполняется в одном потоке. Движок (v8) не имеет возможности поставить обработку события на паузу, перейти на другое действие, а впоследствии — восстановить выполнение первого. Все действия обрабатываются в порядке очереди. Для выполнения кода выделяется область памяти — stack (стэк). Из списка происходящих событий формируется очередь. Как только стэк освобождается — движок может выполнять отложенный код (из очереди). За вызов процессов из очереди и отвечает event loop.

Если вы хотите более детально разобраться с тем, как он работает — рекомендую следующее видео или статью.

Мы определили, что синхронный код выполняется в первую очередь, но почему же Promise.resolve().then() выполнился раньше чем setTimeout, он ведь записан ниже. Напомню порядок выполнения:

Дело в том, что для движка существует два типа задач — макрозадачи (macrotasks) и микрозадачи (microtasks). Из документации:

Macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I / O, отображение пользовательского интерфейса

Microtasks: process.nextTick, Promises, Object.observe, MutationObserver

Какой будет порядок в нашем случае?

  1. Оба console.log() появляются первым, т.к. это синхронные вызовы.
  2. promise выполняется вторым т. к. это микрозадача и выполняется после текущего синхронного кода
  3. setTimeout выполняется последним, т. к. макрозадача

Более детальный разбор задач можете посмотреть здесь.

Report Page