Понимание среды выполнения JavaScript

Источник: «From Good to Great: Why True Engineers Understand JavaScript Inside Out»
Нюансы среды выполнения JavaScript и почему каждый опытный специалист должен быть хорошо знаком с ними.

Однажды во время собеседования, где вместе с несколькими коллегами я был в жюри интервьюеров, один из них задал вопрос кандидату:

Как JavaScript работает под капотом? Можете ли вы кратко объяснить, как вы понимаете такие элементы времени выполнения, как куча, стек вызовов, цикл событий и так далее?

Я был поражён тем, что подобная тема остаётся загадкой для многих разработчиков JavaScript. И, честно говоря, нельзя претендовать на высокий профессионализм, не имея представления об этом предмете.

Чтобы писать качественный код, необходимо понимать, как этот код работает.

Всегда помните: Ваше программирование может быть настолько хорошим, насколько хорошо вы его понимаете.

Итак, не могли бы вы ответить на вопрос интервью?

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

Благодаря Miro (моя первая попытка создать собственные диаграммы), я проиллюстрировал среду выполнения следующим образом:

Среда выполнения Javascript/JavaScript Runtime Environment
Среда выполнения Javascript/JavaScript Runtime Environment

Теперь давайте разберёмся в каждой его части.

Среда выполнения JavaScript (движок)

Среда выполнения, часто называемая движком JavaScript, — это сложное пространство, в котором происходит вся магия выполнения вашего JavaScript-кода. Различные браузеры и платформы имеют свои специализированные движки: V8 используется в Google Chrome и Node.js; SpiderMonkey стоит за Firefox; JavaScriptCore работает в Safari.

В этой среде выполнения два основных компонента играют важную роль в том, как выполняется ваш код: Куча (Heap) и Стек вызовов (Call Stack).

Куча/Heap

Если вы хотите больше узнать о сборке мусора и о том, как избежать утечек памяти, у меня есть ещё одна статья, которая вам поможет:

Стек Вызовов/Call Stack

В качестве иллюстрации:

function multiply(x, y) {
return x * y;
}

function calculate() {
const value = multiply(5, 3);
console.log(value);
}

calculate();

В данном примере:

  1. Вызывается функция calculate(), помещая свой фрейм в Стек Вызовов.
  2. Внутри calculate() вызывается функция multiply(), добавляющая свой фрейм на вершину стека.
  3. После завершения работы функции multiply() она возвращает значение 15, а её фрейм удаляется из Стека Вызовов.
  4. Функция calculate() продолжает выполнение и записывает значение 15 в консоль. После завершения выполнения её фрейм также удаляется из Стека Вызовов.

Асинхронные механизмы JavaScript

В синхронном мире каждая инструкция должна дождаться завершения предыдущей. Однако для выполнения операций, которые могут занять непредсказуемое количество времени (например, чтение файла или получение данных с сервера), JavaScript использует неблокирующую, асинхронную модель. Эффективность этой модели обеспечивается сочетанием Web API, Очереди Обратных Вызовов (Callback Queue) и Цикла Событий (Event Loop).

Web API

Очередь Обратных Вызовов/Callback Queue (или Очередь Задач/Task Queue)

Цикл Событий/Event Loop

Рассмотрим последовательность:

console.log('First');
setTimeout(function() {
console.log('Second');
}, 0);
console.log('Third');

Как вы думаете, что выводит этот код?

  1. console.log('First') добавляется в Стек Вызовов и выполняется.
  2. Встречается setTimeout. Работа с таймером передаётся в Web API.
  3. Сразу после этого console.log('Third') добавляется в Стек Вызовов и выполняется.
  4. Несмотря на то, что длительность таймера равна 0 миллисекунд, обратный вызов setTimeout (который выводит 'Second') помещается в Очередь Обратных Вызовов.
  5. Цикл Событий, заметив, что Стек Вызовов пуст, а в Очереди Обратных Вызовов есть функция, передаёт обратный вызов в Стек Вызовов.
  6. Наконец, выполняется console.log('Second').

Тогда вывод этого кода будет следующим:

First
Third
Second

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

Посмотрите, как на диаграмме показан этот случай:

Среда выполнения JavaScript с фокусом на Web API
Среда выполнения JavaScript с фокусом на Web API

А как насчёт Очереди Микрозадач/Microtask Queue

Чтобы понять это, рассмотрим следующий пример:

console.log('Start');

setTimeout(() => {
console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
console.log('Promise');
});

console.log('End');

Как вы думаете, каким будет результат?

Вот он:

Start
End
Promise
setTimeout

Такой порядок объясняется тем, что после выполнения синхронного кода (Start и End) Цикл Событий видит, что в Очереди Микрозадач есть промис, и выполняет его раньше, чем setTimeout в Очереди Задач, даже несмотря на то, что задержка setTimeout равна 0.

Давайте посмотрим на диаграмму. Обратите внимание, что промис добавляется движком Javascript, так как это прямой промис, а не созданный через fetch, в этом случае он бы сначала прошёл через Web API.

Среда выполнения Javascript с фокусом на Web API и Промисах
Среда выполнения Javascript с фокусом на Web API и Промисах

Мой заключительный совет

В заключение хочу сказать, что за годы работы с JavaScript я понял следующее: быть отличным разработчиком — это не просто писать код. А действительно понимать, как этот код работает.

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

Тем, кто только начинает работать с этим, может показаться, что вначале будет трудновато. И это совершенно нормально. Обучение требует времени. Но по мере того, как вы будете проводить больше времени с JavaScript, не пытайтесь просто запомнить что-то. Вместо этого постарайтесь представить, как все работает вместе. Это поможет вам решать проблемы и понимать странные вещи, которые могут происходить в вашем коде.

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

Дополнительные материалы

Предыдущая Статья

Оптимизация запросов MySQL

Следующая Статья

Текучая типографика для отзывчивого дизайна