Объяснение работы EventLoop в JavaScript
Перевод статьи Anoop Raveendran JavaScript Event Loop Explained.
«Как JavaScript может быть асинхронным и однопоточным?» Если кратко, то JavaScript однопоточный, а асинхронное поведение не является частью самого языка; вместо этого оно построено на основе него в браузере (или среде программирования) и доступно через браузерные API.
Теперь посмотрим на длинный ответ.
Базовая архитектура

- Heap (куча) — объекты собраны в кучу, которая есть ни что иное, как название для наименее структурированной части памяти.
- Stack (стопка, стек) — репрезентация единственного потока выполнения JavaScript-кода. Вызовы функций помещаются в стек (об этом ниже).
- Browser or Web API’s (браузерные или веб API) — встроены в браузер и способны предоставлять данные из браузера и окружающей компьютерной среды и давать возможность выполнять с ними полезные и сложные вещи. Они не являются частью языка JavaScript, но они построены на его основе и предоставляют вам супер силы, которые можно использовать в JavaScript коде. Например Geolocation API предоставляет доступ к нескольким простым конструкциям JavaScript, которые используются для получения данных о местоположении, так что вы можете, скажем, отобразить своё местоположение на Google Map. В фоновом режиме браузер использует низкоуровневый код (например C++) для связи с оборудованием GPS устройства (или любым другим, доступным для определения данных о местоположении), получения данных о местоположении и возвращения их в среду браузера для использования в вашем коде. Но опять, эта сложность абстрагирована от вас посредством API.
Пример кода 1: Интрига
function main() {
console.log('A')
setTimeout(function exec() {
console.log('B')
}, 0)
console.log('C')
}
main()
// Output
// A
// C
// B
Здесь мы видим функцию main, включающую в себя два console.log, выводящих в консоль A и C. Между ними находится setTimeout, вызов которого выведет в консоль B после ожидания в 0 секунд.

- Вызов функции
mainсначала поместит её в стек (в качестве первого элемента (frame)). Потом браузер поместит в стек первое выражение функцииmain, которое представляет собойconsole.log(‘A’). Это выражение выполняется и, после завершения, удаляется из стека. БукваAвыводится в консоль. - Следующее выражение (
setTimeout()с коллбэкомexec()и временем ожидания в 0 секунд) помещается в стек вызовов и выполнение начинается. ФункцияsetTimeoutиспользует API браузера для задержки вызова предоставленной функции. Элемент (frame) удаляется из стека сразу после завершения передачи таймера браузерному API. console.log(‘C’)помещается в стек, пока в браузере запускается таймер для вызова функцииexec(). В этом конкретном случае, поскольку время ожидания составляет 0 секунд, коллбэк (функцияexec()) будет помещён в message queue (очередь сообщений), сразу после того как браузер его получит (в идеале).- После выполнения последнего выражения функции
main, элементmainудаляется из стека вызовов (call stack), оставляя его пустым. Стек вызовов должен быть пустым, для того чтобы браузер поместил в него элемент из message queue. Именно по этой причине даже если вsetTimeoutуказано время ожидания в 0 секунд, функцияexec()не выполняется, пока не закончится выполнение всех элементов в стеке вызовов. - Теперь функция
exec()помещается в стек вызовов и выполняется. БукваCвыводится в консоль. Вот он — цикл событий (EventLoop) JavaScript.
Таким образом аргумент delay в setTimeout(function, delayTime) не означает точное время задержки, после которого функция выполнится. Он означает минимальное время ожидания, после которого в какой-нибудь момент времени, функция будет вызвана.
Пример кода 2: Более глубокое понимание
function main() {
console.log('A')
setTimeout(function exec() {
console.log('B')
}, 0)
runWhileLoopForNSeconds(3)
console.log('C')
}
main()
function runWhileLoopForNSeconds(sec) {
let start = Date.now(),
now = start
while (now - start < sec * 1000) {
now = Date.now()
}
}
// Output
// A
// C
// B
- Функция
runWhileLoopForNSeconds()делает именно то, что отражено в её названии. Она постоянно проверяет, прошло ли со времени её вызова то количество секунд, которое передано аргументом. Главное, что нужно помнить — что циклwhileявляется блокирующим выражением, и это означает, что его выполнение происходит в стеке вызовов и не использует браузерные API. Таким образом он блокирует все последующие выражения, пока не выполнится до конца. - В коде выше, даже не смотря на то, что
setTimeoutимеет задержку в 0 секунд и циклwhileвыполняется 3 секунды, функцияexec()застрянет в очереди сообщений. Циклwhileбудет выполняться в стеке вызовов (в котором один поток), пока не пройдет 3 секунды. И только после того, как стек вызовов опустеет, функцияexec()будет помещена в стек и выполнена. - Таким образом аргумент
delayвsetTimeout()не гарантирует начала выполнения после завершения указанной задержки. Он является минимальным временем задержки.



















