«Это первый день моего участия в августовском испытании обновлений. Подробную информацию об этом событии см. в разделе: августовское испытание обновлений».Наггетс.Талант/пост/698796…
1. Принцип асинхронного выполнения
(1) Однопоточный JavaScript
Мы знаем, что JavaScript — этоодин потокЯзык, который в основном используется для взаимодействия с пользователем и управления DOM.
В JavaScript есть концепция синхронности и асинхронности, которая решает проблему блокировки кода:
- Синхронизировать: если вызывающая сторона может получить ожидаемый результат при возврате функции, то функция является синхронной;
- асинхронный: если вызывающая сторона не может получить ожидаемый результат при возврате функции, но должна получить его определенными средствами в будущем, то функция является асинхронной.
Итак, в чем преимущество однопоточности?
- Отрисовка пользовательского интерфейса может быть заблокирована во время работы JS, что означает, что два потока являются взаимоисключающими. Это связано с тем, что JS может изменять DOM, и если поток пользовательского интерфейса все еще работает во время выполнения JS, это может сделать пользовательский интерфейс небезопасным.
- Благодаря тому, что JS работает в одном потоке, можно добиться преимуществ экономии памяти и сокращения времени переключения контекста.
(2) Многопоточный браузер
JS является однопоточным и может выполнять только одно действие за раз.Так почему же браузеры могут выполнять асинхронные задачи одновременно?
Это связано с тем, что браузер является многопоточным.Когда JS необходимо выполнить асинхронную задачу, браузер запустит другой поток для выполнения задачи. Другими словами, JavaScript является однопоточным, что означает, что существует только один поток, который выполняет код JavaScript, который является потоком механизма JavaScript (основным потоком), предоставляемым браузером. Кроме того, в браузере также есть потоки, такие как потоки таймера и потоки HTTP-запросов, которые в основном не используются для выполнения кода JS.
Например, если запрос данных необходимо отправить в основном потоке, задача будет передана для выполнения асинхронному потоку HTTP-запроса.После того, как данные запроса будут возвращены, обратный вызов JS, который необходимо выполнить в обратном вызове, быть переданы потоку движка JS для выполнения. То есть браузер — это роль, которая фактически выполняет задачу отправки запроса, а JS отвечает только за выполнение окончательной обработки обратного вызова. Так что асинхронность здесь реализована не самим JS, а возможностью, предоставленной ему браузером.
На следующем рисунке представлена схема архитектуры браузера Chrome:
Как видите, в Chrome есть не только несколько процессов, но и несколько потоков. Взяв в качестве примера процесс рендеринга, он включает в себя поток рендеринга GUI, поток механизма JS, поток триггера события, поток триггера таймера и поток асинхронного HTTP-запроса. Эти потоки обеспечивают основу для JS для выполнения асинхронных задач в браузере.
2. Цикл событий браузера
Задачи JavaScript делятся на две同步а также异步:
- Синхронизация задач:Для задач, поставленных в очередь на выполнение в основном потоке, может быть выполнена только одна задача, прежде чем может быть выполнена следующая задача.
- Асинхронные задачи:Вместо входа в основной поток она помещается в очередь задач.Если есть несколько асинхронных задач, они должны ждать в очереди задач.Очередь задач аналогична буферу.Очередная задача будет перемещена в стек выполнения и основной поток будет выполнять стек вызовов task.
Очередь задач и стек выполнения упоминались выше, давайте сначала рассмотрим эти две концепции.
(1) Стек выполнения и очередь задач
1) стек исполнения: Как видно из названия, стек выполнения использует структуру стека в структуре данных, которая является структурой стека для хранения вызовов функций.первый вышел последнимправила.Он в основном отвечает за отслеживание всего кода, который должен быть выполнен.Всякий раз, когда функция завершает выполнение, функция завершения выполнения извлекается из стека; если есть код, который необходимо выполнить, выполняется операция проталкивания. Следующий рисунок является примером:
При выполнении этого кода сначала выполняется основная функция, а затем выполняется наш код. согласно спервый вышел последнимСогласно принципу, функция, которая выполняется позже, будет выталкиваться из стека первой.Так же на рисунке видно, что функция foo выполняется позже, и она будет выталкиваться из стека, когда выполнение будет завершено.
Когда JavaScript последовательно выполняет методы в стеке выполнения, каждый раз при выполнении метода для него будет генерироваться уникальная среда выполнения (контекст), а когда выполнение метода завершится, текущая среда выполнения будет уничтожена. метод всплывает и переходит к следующему методу.
2) Очередь задач:Как видно из названия, очередь задач использует структуру очереди в структуре данных, которая используется для сохранения асинхронных задач и следует запервым прибыл, первым обслуженправила.В основном он отвечает за отправку новых задач в очередь для обработки.
Когда JavaScript выполняет код, он устраивает синхронизированный код в стеке исполнения в порядке, а затем выполняет в нем функции в свою очередь. Когда возникает асинхронная задача, она введена в очередь задач, и после того, как все синхронное выполнение кода выполнение текущего стека выполнения завершено, обратный вызов завершенной асинхронной задачи будет извлечен из очереди асинхронной задачи и ввести в Стек выполнения. Продолжайте выполнять, и так далее, пока все задачи не будут выполнены.
Порядок выполнения задач JavaScript следующий:
В управляемом событиями режиме включается по крайней мере один цикл выполнения для проверки наличия новых задач в очереди задач. При непрерывном цикле обратный вызов асинхронной задачи передается для выполнения.Этот процесс является циклом событий, а каждый цикл является циклом событий.
(2) Макрозадачи и микрозадачи
На самом деле существует более одной очереди задач.В соответствии с различными типами задач ее можно разделить наочередь микро задача такжеочередь макросов. Общие задачи следующие:
- Задача макроса:script (общий код), setTimeout, setInterval, ввод-вывод, события взаимодействия с пользовательским интерфейсом, setImmediate (среда Node.js)
- Микрозадачи:Promise, MutaionObserver, process.nextTick (среда Node.js);
Порядок очереди задач следующий:
Видно, что при реализации логики обработки макросов и микрозадач Eventloop следующим образом:
- Движок JavaScript сначала берет первую задачу из очереди макросов;
- После завершения выполнения вынуть все задачи в микрозадачах и выполнить их последовательно (сюда входят не только микрозадачи в очереди на момент начала выполнения), если на этом шаге генерируются новые микрозадачи, их тоже нужно выполнять.То есть новые микрозадачи, сгенерированные во время выполнения микрозадач, не будут перенесены на следующий цикл для выполнения, а продолжат выполняться в текущем цикле.
- Затем возьмите следующую из очереди задач макроса.После завершения выполнения снова выньте все очереди микрозадач, и цикл повторяется до тех пор, пока не будут удалены все задачи в двух очередях.
То есть цикл Eventloop будет обрабатывать макрозадачу и все микрозадачи, сгенерированные в этом цикле.
Вот пример, чтобы понять цикл событий:
console.log('同步代码1');
setTimeout(() => {
console.log('setTimeout')
}, 0)
new Promise((resolve) => {
console.log('同步代码2')
resolve()
}).then(() => {
console.log('promise.then')
})
console.log('同步代码3');
Вывод кода выглядит следующим образом:
"同步代码1"
"同步代码2"
"同步代码3"
"promise.then"
"setTimeout"
Каков процесс выполнения этого кода?
- Встречайте первую консоль, которая является синхронным кодом, присоединяйтесь к стеку выполнения, выполняйте и извлекайте стек и распечатывайте"код синхронизации 1";
- Когда встречается setTimeout, это задача макроса, которая добавляется в очередь задач макроса;
- Когда вы сталкиваетесь с консолью в новом промисе, это синхронный код, который добавляется в стек выполнения, выполняется и извлекается из стека и распечатывается."код синхронизации 2";
- При встрече с Promise это микрозадача, которая присоединяется к очереди микрозадач;
- Встречайте третью консоль, которая является синхронным кодом, присоединяйтесь к стеку выполнения, выполняйте и извлекайте стек и распечатывайте"код синхронизации 3";
- В этот момент стек выполнения пуст, выполните все задачи в очереди микрозадач и распечатайте"обещаю.потом";
- После выполнения задач в очереди микрозадач выполните задачу в очереди макрозадач и распечатайте"setTimeout"
Примечание. Дополнительные примеры выполнения асинхронного кода см. в статье: «Результаты вывода сводного кода высокочастотных предварительных интервью «2021»»
Из приведенного выше рабочего процесса макрозадач и микрозадач можно сделать следующие выводы:
- Микрозадачи и макрозадачи связаны, и каждая макрозадача будет создавать свою собственную очередь микрозадач при ее выполнении.
- Продолжительность выполнения микрозадачи повлияет на продолжительность текущей макрозадачи. Например, при выполнении макрозадачи генерируется 10 микрозадач, и время выполнения каждой микрозадачи составляет 10 мс, то время выполнения этих 10 микрозадач составляет 100 мс.Также можно сказать, что эти 10 микрозадач составляют макрозадачу. Время выполнения увеличено на 100 мс.
- В макрозадаче создать макрозадачу и микрозадачу для обратного вызова соответственно, в любом случае микрозадача выполняется раньше, чем макрозадача (более высокий приоритет).
Таким образом, вопрос заключается в том, почему задача очередей задач должна быть разделена на задачу Micro и Macro, то в чем разница между ними?
JavaScript Когда вы сталкиваетесь с асинхронной задачей, JavaScript будет обрабатывать эту задачу для других потоков (например, если вы столкнетесь с задачей settimeout, передайте ее потоку триггера таймера для выполнения, конец, поместит задачу обратного вызова таймера в очередь задач. Подождите для выполнения основным потоком), основной поток будет продолжать выполнять следующие задачи синхронизации.
Для микрозадач, таких как promise.then, при выполнении promise.then механизм браузера не будет передавать асинхронную задачу другим потокам браузера для выполнения, а будет сохранять обратный вызов задачи в очереди, когда задача в стеке выполняется. После выполнения выполните очередь микрозадач, где находится promise.then.
Таким образом, существенные различия между макрозадачами и микрозадачами заключаются в следующем:
- Микрозадачи: для выполнения не требуется никаких конкретных асинхронных потоков, никаких явных асинхронных задач для выполнения, только обратные вызовы;
- Задача макроса: для выполнения требуется определенный асинхронный поток, есть четкая асинхронная задача для выполнения и есть обратный вызов;
3. Цикл событий Node.js
(1) Концепция петли событий
Для цикла событий Node.js описание на официальном сайте выглядит следующим образом:
When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call
process.nextTick(), then begins processing the event loop.
Перевод: когда Node.js запускается, он инициализирует цикл событий для обработки входящих скриптов, которые могут выполнять асинхронные вызовы API, планировать таймеры или вызывать process.nextTick(), а затем запускать цикл обработки событий.
JavaScript и Node.js основаны на движке V8, а асинхронные методы, включенные в браузер, такие же, как и в NodeJS. В дополнение к этому в Node.js есть и другие формы асинхронности:
-
Файловый ввод-вывод: асинхронная загрузка локальных файлов.
-
setImmediate(): Аналогично настройке setTimeout 0 мс, она выполняется сразу после завершения некоторых задач синхронизации.
-
process.nextTick(): выполняется сразу после завершения некоторых задач синхронизации.
-
server.close, socket.on('close',...) и т. д.: обратный вызов закрытия.
Выполнение этих асинхронных задач должно опираться на механизм цикла событий Node.js.
Циклы событий в Node.js и браузерах — это совершенно разные вещи. Node.js использует V8 в качестве механизма синтаксического анализа js и использует собственную разработанную библиотеку libuv для обработки ввода-вывода.libuv — это управляемый событиями кроссплатформенный уровень абстракции, который инкапсулирует некоторые базовые функции различных операционных систем и предоставляет унифицированный API для внешний мир. , в нем также реализован механизм событийного цикла, как показано на следующем рисунке:Согласно приведенному выше рисунку вы можете видеть, что механизм работы Node.js выглядит следующим образом:
- Движок V8 отвечает за разбор скриптов JavaScript;
- Разобранный код вызывает Node API;
- Библиотека libuv отвечает за реализацию Node API. Он будет назначать разные задачи разным потокам, чтобы сформировать цикл событий (цикл событий), асинхронным образом результаты выполнения задачи возвращаются к движку V8;
- Движок V8 возвращает результат пользователю;
(2) Поток событийного цикла
Цикл событий в движке libuv разделен на 6 этапов, которые последовательно выполняются многократно. Всякий раз, когда вводится определенный этап, функция будет извлечена из соответствующей очереди обратного вызова для выполнения. Когда очередь опустеет или количество выполненных callback-функций достигнет порога, установленного системой, произойдет переход к следующему этапу. Ниже приведен поток цикла обработки событий Eventloop:
Весь процесс разбит на шесть этапов, когда эти шесть этапов выполняются один раз, это можно рассматривать как зацикленный процесс Eventloop. Вот посмотрите, что делают шесть этапов:
-
timersСтадия: выполнить обратный вызов таймера (setTimeout, setInterval), который контролируется стадией опроса; -
I/O callbacksСтадия: в основном выполнять функции обратного вызова системного уровня, такие как обратный вызов сбоя TCP-соединения; -
idle, prepareСтадия: используется только внутри Node.js и может быть проигнорирована; -
pollСтадия: опрос событий, таких как новые ссылки и запросы, выполнение обратных вызовов ввода-вывода и т. д.; -
checkЭтап: выполнить обратный вызов setImmediate(); -
close callbacksЭтап: выполнить функцию обратного вызова запроса на закрытие, например socket.on('close', ...)
Примечание: Каждый из вышеперечисленных этапов будет выполнять очередь задач текущего этапа, а затем продолжит выполнение очереди микрозадач текущего этапа.Только когда все микрозадачи на текущем этапе будут выполнены, следующий этап будет выполнен. введено, что тоже совпадает с логикой в браузере.Где большая разница.
Среди них наиболее важным является четвертый этап: опрос, На этом этапе система в основном делает две вещи:
- Вернитесь к фазе таймера, чтобы выполнить обратный вызов
- Выполнение обратных вызовов ввода/вывода
Если таймер не установлен при входе на этот этап, произойдет следующее:
(1) Если очередь опроса не пуста, очередь обратного вызова будет проходиться и выполняться синхронно до тех пор, пока очередь не станет пустой или не будет достигнут системный предел;
(2) Если очередь опроса пуста, произойдет следующее:
- Если необходимо выполнить обратный вызов setImmediate, фаза опроса остановится и перейдет к фазе проверки для выполнения обратного вызова;
- Если обратный вызов setImmediate не будет выполнен, он будет ждать добавления обратного вызова в очередь и немедленно выполнит обратный вызов.Также будет установлен тайм-аут, чтобы предотвратить вечное ожидание;
Когда таймер установлен и очередь опроса пуста, будет оцениваться, есть ли тайм-аут таймера, и если есть, он вернется к стадии таймера для выполнения обратного вызова.
Конкретный поток выполнения этого процесса показан на следующем рисунке:
(3) Макрозадачи и микрозадачи
Асинхронная очередь цикла событий Node.js также делится на два типа: очередь макрозадач и очередь микрозадач.
- Общие задачи макросов: setTimeout, setInterval, setImmediate, сценарий (общий код), операции ввода-вывода и т. д.
- Общие микрозадачи: process.nextTick, new Promise().then(callback) и т. д.
(4) процесс.nextTick()
Вышеупомянутый process.nextTick(), который представляет собой новую очередь задач в узле, которая будет выполняться непосредственно перед переходом на следующий этап в конце каждого из вышеперечисленных этапов.
Официальная документация Node.js объясняет следующее:
process.nextTick()is not technically part of the event loop. Instead, thenextTickQueuewill be processed after the current operation is completed, regardless of the current phase of the event loop. Here, an operation is defined as a transition from the underlying C/C++ handler, and handling the JavaScript that needs to be executed.
Например следующий код:
setTimeout(() => {
console.log('timeout');
}, 0);
Promise.resolve().then(() => {
console.error('promise')
})
process.nextTick(() => {
console.error('nextTick')
})
Результат выглядит следующим образом:
nextTick
promise
timeout
Как видите, process.nextTick() — это выполнение обратного вызова, которое имеет приоритет над промисами.
(5) setImmediate и setTimeout
Выше также упоминались setImmediate и setTimeout, которые очень похожи, основное отличие заключается во времени вызова:
- setImmediate: выполняется, когда фаза опроса завершена, то есть фаза проверки;
- setTimeout: когда стадия опроса простаивает и достигается установленное время, она выполняется, но выполняется на стадии таймера;
Например следующий код:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
Результат выглядит следующим образом:
timeout
setImmediate
Во время выполнения приведенного выше кода, после первого раунда цикла, setTimeout и setImmediate добавляются в очереди задач соответствующих этапов. Сначала вступает во второй раунд циклаэтап таймеров, выполните обратный вызов очереди таймера, затемpending callbacksа такжеэтап опросанет задачи, так что введитеэтап проверкиВыполните обратный вызов setImmediate. Таким образом, окончательный вывод — тайм-аут, setImmediate.
4. Разница между узлом и циклом событий браузера
Разница между Node.js и циклом событий браузера заключается в следующем:
- Node.js: микрозадачи выполняются между этапами цикла событий;
- Браузер: микрозадача выполняется после выполнения макрозадачи цикла событий;
Сравнение процесса цикла событий Nodejs и браузера выглядит следующим образом:
- Выполнить глобальный код скрипта (такой же, как в браузере);
- Очистите очередь микрозадач. Обратите внимание, что метод очистки очереди микрозадач в Node уникален. В браузере у нас есть только одна очередь микрозадач для обработки, но в Node есть два типа очередей микрозадач: очереди следующего тика и другие очереди. Среди них очередь следующего тика специально используется для объединения асинхронных задач, отправляемых process.nextTick.При очистке очереди сначала очищаются задачи в очереди следующего тика, а затем другие микрозадачи.;
- Начать выполнение макрозадачи. Обратите внимание, что способ выполнения макрозадач Node отличается от браузера: в браузере мы удаляем из очереди и каждый раз выполняем одну макрозадачу; в Node мы каждый раз пытаемся очистить все задачи в очереди макрозадач, соответствующих текущему этапу. (если не достигнут системный лимит);
- Начиная с шага 3, он войдет в цикл 3 -> 2 -> 3 -> 2….