Объяснение работы 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()
не гарантирует начала выполнения после завершения указанной задержки. Он является минимальным временем задержки.