Введение
JavaScript в основном выполняется в одном потоке в Node.js и в браузере (за некоторыми исключениями, такими как рабочие потоки, которые выходят за рамки текущей статьи). В этой статье я попытаюсь объяснить механизм параллелизма в Node.js, который представляет собой цикл событий.
Прежде чем начать читать эту статью, вы должны быть знакомы со стеком и тем, как он работает, я уже писал об этой идее ранее, поэтому ознакомьтесь со Stack & Heap — Не начинайте программировать, не понимая их — Моше Биниели | Середина
Вступительное изображение
Примеры
Я считаю, что лучше всего учиться на примерах, поэтому начну с 4 простых примеров кода. Я проанализирую примеры, а затем углублюсь в архитектуру Node.js.
Пример 1:
console.log(1);
console.log(2);
console.log(3);
// Выход:
// 1
// 2
// 3
Этот пример довольно простой: на первом этапе console.log(1) попадает в стек вызовов, выполняется и затем удаляется, на втором этапе console.log(2) попадает в стек вызовов и выполняется а затем удален и т. д. для console.log(3).
Визуализация стека вызовов для примера 1
Пример 2:
console.log(1);
setTimeout(функция foo(){
console.log(2);
}, 0);
console.log(3);
// Выход:
// 1
// 3
// 2
В этом примере мы видим, что мы запускаем setTimeout сразу, поэтому мы ожидаем, что console.log(2) будет раньше console.log(3), но это не так, и давайте разберемся в механизме, лежащем в основе этого.
Базовая архитектура цикла событий (подробнее мы углубимся в нее позже)
Stack & Heap: ознакомьтесь с моей статьей об этом (я добавил ссылку в начале этой статьи)
Веб-APIS: они встроены в ваш веб-браузер и могут предоставлять данные из браузера и окружающей компьютерной среды и выполнять с ними сложные полезные действия. Они не являются частью самого языка JavaScript, а построены на основе основного языка JavaScript, предоставляя вам дополнительные возможности для использования в вашем коде JavaScript. Например, API геолокации предоставляет несколько простых конструкций JavaScript для получения данных о местоположении, чтобы вы могли, например, нанести свое местоположение на карту Google. В фоновом режиме браузер фактически использует некоторый сложный код нижнего уровня (например, C) для связи с оборудованием GPS устройства (или чем-то еще, доступным для определения данных о местоположении), получения данных о местоположении и возврата их в среду браузера для использования. в вашем коде. Но опять же, эта сложность абстрагируется от вас с помощью API.
Цикл событий и очередь обратных вызовов. Функции, завершившие выполнение Web Apis, перемещаются в очередь обратных вызовов. Это обычная структура данных очереди, а цикл событий отвечает за исключение следующей функции из очереди обратных вызовов и отправку функции в стек вызовов для выполнения функции.
Порядок выполнения
Все функции, которые в данный момент находятся в стеке вызовов, выполняются, а затем извлекаются из стека вызовов.
Когда стек вызовов пуст, все задачи, стоящие в очереди, помещаются в стек вызовов одна за другой и выполняются, а затем извлекаются из стека вызовов.
Давайте разберемся с примером 2
Метод console.log(1) вызывается, помещается в стек вызовов и выполняется.
Метод setTimeout вызывается, помещается в стек вызовов и выполняется. Это выполнение создает новый вызов setTimeout Web Api на 0 миллисекунд, когда он завершается (сразу, или, если быть более точным, то это будет лучше сказать «как можно скорее»), веб-API перемещает вызов в очередь обратного вызова.
console.log(3) метод вызывается, помещается в стек вызовов и выполняется.
Цикл событий видит, что стек вызовов пуст, извлекает метод «foo» из очереди обратного вызова и помещает его в стек вызовов, после чего выполняется console.log(2).
Визуализация процесса для примера 2
Таким образом, параметр задержки в setTimeout(function, Delay) не означает точную задержку времени, после которой выполняется функция. Это минимальное время ожидания, по истечении которого в какой-то момент функция будет выполнена.
Пример 3:
console.log(1);
setTimeout(функция foo() {
console.log('foo');
}, 3500);
setTimeout(функция boo() {
console.log('бу');
}, 1000);
console.log(2);
// Выход:
// 1
// 2
// 'бу'
// 'фу'
Визуализация процесса для примера 3
Пример 4:
console.log(1);
setTimeout(функция foo() {
console.log('foo');
}, 6500);
setTimeout(функция boo() {
console.log('бу');
}, 2500);
setTimeout(функция baz() {
console.log('baz');
}, 0);
for (константное значение [’A’, ‘B’]) {
console.log(значение);
}
функция два() {
console.log(2);
}
два();
// Выход:
// 1
// 'А'
// 'Б'
// 2
// 'баз'
// 'бу'
// 'фу'
Визуализация процесса для примера 4
Цикл событий приступает к выполнению всех обратных вызовов, ожидающих в очереди задач. Внутри очереди задач задачи делятся на две категории, а именно микрозадачи и макрозадачи.
Макрозадачи (очередь задач) и микрозадачи
Если быть более точным, на самом деле существует два типа очередей.
Есть еще несколько задач, которые попадают в очередь макрозадач и очередь микрозадач, но я расскажу об наиболее распространенных.
Общими макрозадачами являются setTimeout, setInterval и setImmediate.
Распространенными микрозадачами являются процесс.nextTick и обратный вызов Promise.
Порядок выполнения
Все функции, которые в данный момент находятся в стеке вызовов, выполняются, а затем извлекаются из стека вызовов.
Когда стек вызовов пуст, все микрозадачи в очереди помещаются в стек вызовов одна за другой и выполняются, а затем извлекаются из стека вызовов.
Когда и стек вызовов, и очередь микрозадач пусты, все макрозадачи, находящиеся в очереди, помещаются в стек вызовов одна за другой и выполняются, а затем извлекаются из стека вызовов.
Пример 5:
console.log(1);
setTimeout(функция foo() {
console.log('foo');
}, 0);
Обещание.resolve()
.then(function boo() {
console.log('бу');
});
console.log(2);
// Выход:
// 1
// 2
// 'бу'
// 'фу'
Метод console.log(1) вызывается, помещается в стек вызовов и выполняется.
SetTimeout выполняется, console.log('foo') перемещается в SetTimeout Web Api, а через 0 миллисекунд после этого он перемещается в очередь макрозадач.
Вызывается Promise.resolve(), он разрешается, а затем метод .then() перемещается в очередь микрозадач.
Метод console.log(2) вызывается, помещается в стек вызовов и выполняется.
Цикл событий видит, что стек вызовов пуст, он сначала берет задачу из очереди микрозадач, которая является задачей Promise, помещает console.log('boo') в стек вызовов и выполняет ее.
Цикл событий видит, что стек вызовов пуст, затем видит, что микрозадача пуста, затем берет следующую задачу из очереди макрозадач, которая является задачей SetTimeout, помещает console.log('foo') в стеке вызовов и выполняет его.
Визуализация процесса для примера 5
Продвинутое понимание цикла событий
Я подумывал написать о низком уровне работы механизма цикла событий, это могло бы быть отдельным постом, поэтому я решил представить введение в тему и прикрепить хорошие ссылки, которые подробно объясняют эту тему.
Описание нижнего уровня цикла событий
Когда Node.js запускается, он инициализирует цикл событий, обрабатывает предоставленный входной сценарий (или помещает его в REPL), который может выполнять асинхронные вызовы API, планировать таймеры или вызывать процесс.nextTick(), а затем начинает обработку цикла событий.
На следующей диаграмме показан упрощенный обзор порядка операций цикла событий. (Каждое поле будет называться «фазой» цикла событий. Чтобы лучше понять цикл, ознакомьтесь со вступительным изображением.)
Упрощенный обзор порядка операций цикла событий
На каждой фазе имеется очередь обратных вызовов FIFO для выполнения (я говорю об этом осторожно, поскольку в зависимости от реализации может существовать другая структура данных). Хотя каждая фаза по-своему особенная, обычно, когда цикл событий входит в данную фазу, он выполняет любые операции, специфичные для этой фазы, а затем выполняет обратные вызовы в очереди этой фазы до тех пор, пока очередь не будет исчерпана или не будет выполнено максимальное количество обратных вызовов. выполнил. Когда очередь исчерпана или достигнут предел обратного вызова, цикл событий перейдет к следующей фазе и так далее.
Обзор этапов
Таймеры: на этом этапе выполняются обратные вызовы, запланированные с помощью setTimeout() и setInterval().
Ожидающие обратные вызовы: выполняет обратные вызовы ввода-вывода, отложенные до следующей итерации цикла.
Режим ожидания, подготовка: используется только для внутреннего использования.
Опрос: получение новых событий ввода-вывода; выполнить обратные вызовы, связанные с вводом-выводом (почти все, за исключением обратных вызовов закрытия, запланированных таймерами и setImmediate()); узел будет блокироваться здесь, когда это необходимо.
Проверьте: здесь вызываются обратные вызовы setImmediate().
Обратные вызовы закрытия: некоторые обратные вызовы закрытия, например. сокет.on('закрыть', ...).
Как сюда вписываются предыдущие шаги?
Таким образом, предыдущие шаги только с «Очередью обратного вызова», а затем с «Макро- и микроочередями» представляли собой абстрактные объяснения того, как работает цикл событий.
Есть еще одна ВАЖНАЯ вещь, которую следует упомянуть: цикл событий должен полностью обрабатывать очередь микрозадач после обработки одной макрозадачи из очереди макрозадач.
Шаг 1: Цикл событий обновляет время цикла до текущего времени для текущего выполнения.
Шаг 2: Микроочередь выполняется.
Шаг 3. Задача из этапа «Таймеры» выполняется.
Шаг 4: Проверка наличия чего-либо в микроочереди и выполнение всей микроочереди, если что-то есть.
Шаг 5: Возврат к шагу 3 до тех пор, пока фаза «Таймеры» не станет пустой.
Шаг 6: Выполняется задача из фазы ожидающих обратных вызовов.
Шаг 7: Проверка наличия чего-либо в микроочереди и выполнение всей микроочереди, если что-то есть.
Шаг 8: Возврат к шагу 6 до тех пор, пока фаза ожидающих обратных вызовов не станет пустой.
А затем Idle… Микроочередь… Опрос… Микроочередь… Проверка… Микроочередь… Закрытие обратных вызовов, и все начинается заново.
Итак, я дал хороший обзор того, как на самом деле работает цикл событий за кулисами. Есть много недостающих частей, о которых я не упомянул здесь, потому что фактическая документация отлично объясняет это. Я предоставлю отличные ссылки для документации, я советую вам потратить 10–20 минут и разобраться в ней.
Отказ от ответственности: Все предоставленные ресурсы частично взяты из Интернета. В случае нарушения ваших авторских прав или других прав и интересов, пожалуйста, объясните подробные причины и предоставьте доказательства авторских прав или прав и интересов, а затем отправьте их по электронной почте: [email protected]. Мы сделаем это за вас как можно скорее.
Copyright© 2022 湘ICP备2022001581号-3