Объяснение работы EventLoop в JavaScript

4315

Объяснение работы EventLoop в JavaScript

Перевод статьи Anoop Raveendran JavaScript Event Loop Explained.

«Как JavaScript может быть асинхронным и однопоточным?» Если кратко, то JavaScript однопоточный, а асинхронное поведение не является частью самого языка; вместо этого оно построено на основе него в браузере (или среде программирования) и доступно через браузерные API.

Теперь посмотрим на длинный ответ.

Базовая архитектура

Объяснение работы EventLoop в JavaScript
Объяснение работы EventLoop в JavaScript
  • 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 секунд.

Вот что происходит внутри во время исполнения
Вот что происходит внутри во время исполнения
  1. Вызов функции main сначала поместит её в стек (в качестве первого элемента (frame)). Потом браузер поместит в стек первое выражение функции main, которое представляет собой console.log(‘A’). Это выражение выполняется и, после завершения, удаляется из стека. Буква A выводится в консоль.
  2. Следующее выражение (setTimeout() с коллбэком exec() и временем ожидания в 0 секунд) помещается в стек вызовов и выполнение начинается. Функция setTimeout использует API браузера для задержки вызова предоставленной функции. Элемент (frame) удаляется из стека сразу после завершения передачи таймера браузерному API.
  3. console.log(‘C’) помещается в стек, пока в браузере запускается таймер для вызова функции exec(). В этом конкретном случае, поскольку время ожидания составляет 0 секунд, коллбэк (функция exec()) будет помещён в message queue (очередь сообщений), сразу после того как браузер его получит (в идеале).
  4. После выполнения последнего выражения функции main, элемент main удаляется из стека вызовов (call stack), оставляя его пустым. Стек вызовов должен быть пустым, для того чтобы браузер поместил в него элемент из message queue. Именно по этой причине даже если в setTimeout указано время ожидания в 0 секунд, функция exec() не выполняется, пока не закончится выполнение всех элементов в стеке вызовов.
  5. Теперь функция 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() не гарантирует начала выполнения после завершения указанной задержки. Он является минимальным временем задержки.

источник