Общий обзор того, что происходит во время выполнения.

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

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

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

Анатомия

Есть несколько объектов, которые выполняются во время выполнения, а именно: стек вызовов, куча, очередь задач, очередь микрозадач и самое главное, цикл событий.

При выполнении вашего JavaScript у нас есть 2 области памяти, стек вызовов и куча.

Стек вызовов

Это структура данных, которая используется для выполнения функций в нашем коде. Всякий раз, когда функция должна быть вызвана, она помещается в стек, и после завершения выполнения она извлекается из стека.

При вызове функции в стек вызовов добавляется кадр с копией локальной переменной этой функции. Если внутри этой функции вызывается другая функция, в стек вызовов добавляется еще один кадр. В противном случае, если функция возвращает значение, кадр удаляется из стека.

Рассмотрим следующий пример кода.

function eat() {
  var cutlery = "🥄🥄🥄"; // Local variable
  return makeFood("🍞"); // Push
}

function makeFood(food) {
  return food; // Pop
}

eat();

Для этого воспользуйтесь отладчиком браузера для проверки стека вызовов:

Вы также можете просмотреть этот стек, когда получите сообщение об ошибке StackTrace. Кроме того, проблема StackOverflow связана с тем, что вы превысили доступное пространство в этом стеке.

Если у вас есть асинхронный код, то он перемещается в очередь микрозадач, если это обещание, иначе он перемещается в очередь задач, если и после завершения его выполнения он вызывается для вывода.

куча

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

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

Цикл событий

Длительные задачи обрабатываются циклом обработки событий. Интуитивно это просто цикл while, который ожидает сообщений из очереди и выполняет их синхронный код.

Когда вы нажимаете кнопку с помощью прослушивателя событий щелчка, он отправляет сообщение в очередь, а во время выполнения цикл событий затем выполняет этот обратный вызов.

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

Это делает JavaScript неблокирующим, потому что единственное, что он делает, — это прослушивает события и обрабатывает обратный вызов для функции, поэтому он никогда не ждет возвращаемого значения функции. Единственное, чего он ждет, — это ЦП для обработки синхронного кода (для чего нужны всего микросекунды).

Цикл событий сначала проходит через весь синхронный код и выполняет его. При этом, если он сталкивается с асинхронным кодом, он помещает его в очередь микрозадач (если это обещание), в противном случае он помещает его в очередь задач, где асинхронный код будет выполняться в отдельном потоке, поэтому этот код не будет блокировать основной поток JavaScript.

После синхронного кода JavaScript будет проходить через очередь микрозадач, чтобы проверить, есть ли какие-либо выходные данные. Если задача выполнена, она извлекается из очереди микрозадач и помещается в очередь стека вызовов, откуда она вызывается для вывода. После этого JavaScript переходит в очередь задач и делает то же самое.

Очередь микрозадач имеет приоритет над очередью задач.

Рассмотрим следующий пример:

console.log("Sync 1: 🍩");

setTimeout(() => {
  console.log("Async 1: 🍕");
}, 0);

console.log("Sync 2: 🍔");

Promise.resolve().then(() => {
  console.log("Async 2: 🍟");
});

console.log("Sync 3: 🍦");

Promise.reject().catch(() => {
  console.log("Async 3: 🥪");
});

// Output:
// Sync 1: 🍩
// Sync 2: 🍔
// Sync 3: 🍦
// Async 2: 🍟
// Async 3: 🥪
// Async 1: 🍕

Для каждого раунда цикла событий:

  • Запустить синхронный код
  • Выполнение обратных вызовов очереди микрозадач
  • Запуск обратных вызовов очереди задач