Перевод статьи 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.
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)
не означает точное время задержки, после которого функция выполнится. Он означает минимальное время ожидания, после которого в какой-нибудь момент времени, функция будет вызвана.
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()
не гарантирует начала выполнения после завершения указанной задержки. Он является минимальным временем задержки.
Эта статья была написана под сильным влиянием от доклада, который сделал Philip Roberts (Филип Робертс) — JS Event Loop. Для демонстрации работы цикла событий вы можете перейти на созданный им http://latentflip.com/loupe. Спасибо Филипу за доклад, он помог мне лучше понять JavaScript.
Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.
Если вам понравилась статья, внизу можно поддержать автора хлопками 👏🏻 Спасибо за прочтение!