предисловие
В этой статье мы представим принцип асинхронной реализации в JS и поймем, что Event Loop на самом деле отличается в браузере и Node.
Если вы хотите прочитать больше оригинальных статей высокого качества, пожалуйста, нажмитеБлог GitHub
1. Потоки и процессы
1. Концепция
Мы часто говорим, что JS выполняется в одном потоке, что означает, что в процессе есть только один основной поток, так что же такое поток? Что такое процесс?
Официальное заявление:Процесс — наименьшая единица распределения ресурсов ЦП; поток — наименьшая единица планирования ЦП.. Эти два предложения не так просто понять, давайте сначала посмотрим на картинку:
- Процесс похож на фабрику на рисунке, которая имеет свой собственный эксклюзивный фабричный ресурс.
- Потоки подобны рабочим на рисунке. Несколько рабочих совместно работают на фабрике. Отношение между фабриками и рабочими равно 1:n. то естьПроцесс состоит из одного или нескольких потоков, которые представляют собой разные пути выполнения кода внутри процесса.;
- Заводское пространство разделено рабочими, что символизируетПространство памяти процесса является общим, и каждый поток может использовать эту общую память..
- Несколько заводов существуют независимо.
2. Многопроцессорность и многопоточность
- Многопроцессность: одновременное выполнение двух или более процессов в одной и той же компьютерной системе. Преимущества многопроцессорности очевидны, например, вы можете открывать редактор и набирать код во время прослушивания песни, а процессы редактора и программы для прослушивания совершенно не будут мешать друг другу.
- Многопоточность: программа содержит несколько потоков выполнения, то есть несколько разных потоков могут быть запущены одновременно в программе для выполнения разных задач, то есть одной программе разрешено создавать несколько потоков, выполняющихся параллельно для выполнения соответствующих задач.
Взяв в качестве примера браузер Chrome, когда вы открываете вкладку, вы фактически создаете процесс, и процесс может иметь несколько потоков (подробно описанных ниже), таких как потоки рендеринга, потоки движка JS, потоки HTTP-запросов и т. д. Ждать. Когда вы инициируете запрос, вы фактически создаете поток, а когда запрос завершается, поток может быть уничтожен.
2. Ядро браузера
Проще говоря, ядро браузера является окончательным выводом результатов визуального изображения, получая содержимое страницы, организуя информацию (применяя CSS), вычисляя и комбинируя, и его часто называют механизмом рендеринга.
Ядро браузера является многопоточным. Под управлением ядра каждый поток взаимодействует друг с другом для поддержания синхронизации. Браузер обычно состоит из следующих резидентных потоков:
- Поток рендеринга графического интерфейса
- Поток движка JavaScript
- синхронизированный триггерный поток
- поток триггера события
- Асинхронный поток HTTP-запросов
1. Поток рендеринга графического интерфейса
- В основном отвечает за рендеринг страницы, анализ HTML, CSS, построение дерева DOM, макет и рисование и т. д.
- Этот поток выполняется, когда необходимо перерисовать интерфейс или когда какая-либо операция вызывает перекомпоновку.
- Этот поток является взаимоисключающим с потоком механизма JS. Когда поток механизма JS выполняется, визуализация графического интерфейса пользователя будет приостановлена. Когда очередь задач простаивает, основной поток будет выполнять визуализацию графического интерфейса.
2. Поток движка JS
- Конечно, этот поток в основном отвечает за обработку скриптов JavaScript и выполнение кода.
- Он также в основном отвечает за выполнение событий, готовых к выполнению, то есть, когда счетчик таймера заканчивается или когда асинхронный запрос завершается успешно и возвращается правильно, он по очереди входит в очередь задач и ждет выполнения JS. резьба двигателя.
- Конечно, этот поток является взаимоисключающим с потоком рендеринга GUI.Когда поток движка JS слишком долго выполняет сценарий JavaScript, рендеринг страницы будет заблокирован.
3. Таймер запускает поток
- Поток, отвечающий за выполнение таких функций, как асинхронные таймеры, такие как: setTimeout, setInterval.
- Когда основной поток последовательно выполняет код, он передает таймер потоку для обработки, когда он сталкивается с таймером.Когда подсчет завершен, поток запуска события добавит подсчитанное событие в конец очереди задач и дождитесь выполнения потока JS-движка.
4. Поток триггера события
- В основном отвечает за передачу подготовленных событий в поток JS-движка для выполнения.
Например, когда истекает время таймера setTimeout, асинхронный запрос, такой как ajax, выполняется успешно и запускается функция обратного вызова, или когда пользователь инициирует событие щелчка, поток последовательно добавляет события для отправки в конец очереди задач. , ожидая выполнения потока JS-движка.
5. Асинхронный поток HTTP-запросов
- Поток, отвечающий за выполнение таких функций, как асинхронные запросы, например: Promise, axios, ajax и т. д.
- Когда основной поток последовательно выполняет код и сталкивается с асинхронным запросом, он передает функцию потоку для обработки.При отслеживании изменения кода состояния, если есть функция обратного вызова, поток триггера события добавит обратный вызов в конец очереди задач и дождитесь выполнения потока JS-движка.
3. Цикл событий в браузере
1. Микрозадача и макрозадача
В цикле событий на стороне браузера существует два типа асинхронных очередей: очередь макросов (макрозадач) и микроочередей (микрозадач).Очередей макрозадач может быть несколько, а очередь микрозадач может быть только одна..
- Общие макрозадачи, такие как: setTimeout, setInterval, сценарий (общий код), операции ввода-вывода, рендеринг пользовательского интерфейса и т. д.
- Общие микрозадачи, такие как: new Promise().then (обратный вызов), MutationObserver (новая функция html5) и т. д.
2. Анализ процесса цикла событий
Полный цикл обработки событий можно разделить на следующие этапы:
-
В начале стек выполнения пустой, можно поставитьСтек выполнения считается структурой стека, в которой хранятся вызовы функций в соответствии с принципом «первым поступил — последним обслужен».. Микро-очередь пуста, а в макро-очереди находится только один сценарий (весь код).
-
Глобальный контекст (тег скрипта) помещается в стек выполнения, синхронизируя выполнение кода. В процессе выполнения он определит, является ли это синхронной задачей или асинхронной задачей.При вызове некоторых интерфейсов могут быть сгенерированы новые макрозадачи и микрозадачи, и они будут помещены в соответствующие очереди задач. После выполнения синхронного кода сценарий сценария будет удален из очереди макросов.Этот процесс, по сути, представляет собой процесс выполнения и исключения из очереди макрозадачи очереди.
-
На предыдущем шаге мы удалили из очереди макрозадачу, а на этом шаге мы рассмотрели микрозадачу. Но будьте осторожны: когда макрозадача удаляется из очереди, задачапо одномувыполняется; в то время как микрозадача удалена из очереди, задачакоманда за командойреализовано. Поэтому мы обрабатываем шаг микроочереди, который будет выполнять задачи в очереди одну за другой и удалять ее из очереди до тех пор, пока очередь не будет очищена.
-
Выполнение операций рендеринга, обновление интерфейса
-
Проверьте, есть ли задача веб-воркера, если да, обработайте ее.
-
Описанный выше процесс повторяется до тех пор, пока обе очереди не будут опустошены.
Подытожим, каждый цикл представляет собой такой процесс:
Когда макрозадача выполняется, она проверяет, существует ли очередь микрозадач. Если да, сначала выполните все задачи в очереди микрозадач. Если нет, будет считана верхняя задача в очереди макрозадач. В процессе выполнения макрозадачи, если встречается микрозадача, она будет добавлена в очередь микрозадач в перемена. После того, как стек опустеет, снова прочитайте задачи в очереди микрозадач и так далее.
Далее, давайте рассмотрим пример, чтобы представить описанный выше процесс:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
})
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0)
Окончательный вывод: Promise1, setTimeout1, Promise2, setTimeout2.
- После того, как задача синхронизации (которая относится к задаче макроса) исходного стека выполнения будет завершена, она проверит, существует ли очередь микрозадач, которая существует в вышеуказанном вопросе (есть только одна), а затем выполнит все задачи. в очередь микрозадач выводить Promise1, и в то же время она будет генерировать макро-задачу setTimeout2
- Затем перейдите к проверке очереди задач макроса, задача макроса setTimeout1 выполняет задачу макроса setTimeout1 до setTimeout2 и выводит setTimeout1
- При выполнении макрозадачи setTimeout1 будет сгенерирована микрозадача Promise2 и помещена в очередь микрозадач, затем все задачи в очереди микрозадач будут сначала очищены, и будет выведен Promise2.
- После очистки всех задач в очереди микрозадач она перейдет в очередь задач макросов, чтобы получить еще одну На этот раз выполняется setTimeout2.
4. Цикл событий в узле
1. Введение в узел
Циклы событий в Node и браузерах — это совершенно разные вещи. Node.js использует V8 в качестве механизма синтаксического анализа js и использует libuv, разработанную самостоятельно для обработки ввода-вывода.libuv — это управляемый событиями кроссплатформенный уровень абстракции, который инкапсулирует некоторые базовые функции различных операционных систем и предоставляет унифицированный API для внешний мир. , механизм событийного цикла также является его реализацией (подробно описан ниже).
- Движок V8 анализирует скрипты JavaScript.
- Разобранный код вызывает Node API.
- Библиотека libuv отвечает за выполнение Node API. Он назначает разные задачи разным потокам для формирования цикла событий (цикла событий) и возвращает результат выполнения задачи механизму V8 асинхронным образом.
- Затем движок V8 возвращает результат пользователю.
2. Шесть этапов
Цикл событий в движке libuv разделен на 6 этапов, которые последовательно выполняются многократно. Всякий раз, когда вводится определенный этап, функция будет извлечена из соответствующей очереди обратного вызова для выполнения. Когда очередь опустеет или количество выполненных callback-функций достигнет порога, установленного системой, произойдет переход к следующему этапу.
На приведенном выше рисунке вы можете примерно увидеть порядок цикла событий в узле:
Внешние входные данные --> этап опроса (poll) --> этап проверки (check) --> этап обратного вызова закрытия события (close callback) --> этап обнаружения таймера (timer) --> этап обратного вызова события ввода/вывода (I /O обратные вызовы) --> стадия простоя (ожидание, подготовка) --> стадия опроса (неоднократно выполняться в этом порядке)...
- этап таймеров: на этом этапе выполняется обратный вызов таймера (setTimeout, setInterval)
- Фаза обратных вызовов ввода-вывода: обработать несколько невыполненных обратных вызовов ввода-вывода из предыдущего цикла.
- бездействие, этап подготовки: используется только внутри узла
- Этап опроса: получение новых событий ввода-вывода, здесь узел будет заблокирован при соответствующих условиях.
- фаза проверки: выполнить обратный вызов setImmediate()
- Этап закрытых обратных вызовов: выполнить обратный вызов события закрытия сокета
Уведомление:Ни один из шести вышеперечисленных этапов не включает process.nextTick().(будет представлен ниже)
Далее мы подробно знакомимtimers
,poll
,check
Эти три этапа, потому что большинство асинхронных задач в ежедневной разработке обрабатываются на этих трех этапах.
(1) timer
Этап таймеров выполняет обратные вызовы setTimeout и setInterval и управляется этапом опроса. такой же,Время, указанное таймером в узле, не является точным временем, его можно выполнить только как можно скорее..
(2) poll
опрос является ключевым этапом, на этом этапе система будет делать две вещи
1. Вернитесь к этапу таймера, чтобы выполнить обратный вызов
2. Выполните обратный вызов ввода-вывода
И если таймер не установлен при входе на этот этап, произойдут следующие две вещи.
- Если очередь опроса не пуста, очередь обратного вызова будет проходиться и выполняться синхронно до тех пор, пока очередь не станет пустой или не будет достигнут системный предел.
- Если очередь опроса пуста, происходят две вещи
- Если необходимо выполнить обратный вызов setImmediate, фаза опроса остановится и перейдет к фазе проверки для выполнения обратного вызова.
- Если обратный вызов setImmediate не выполняется, он будет ждать добавления обратного вызова в очередь и немедленно выполнит обратный вызов.Также будет установлен тайм-аут, чтобы предотвратить его ожидание.
Конечно, если таймер установлен и очередь опроса пуста, он определит, есть ли тайм-аут таймера, и если да, то вернется на этап таймера для выполнения обратного вызова.
(3) этап проверки
Обратный вызов setImmediate() будет добавлен в очередь проверки.Как видно из диаграммы этапов событийного цикла, порядок выполнения этапа проверки следующий за этапом опроса. Сначала рассмотрим пример:
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
- В начале выполняется задача синхронизации стека выполнения (это относится к задаче макроса) (по очереди распечатывается начало конца, а два таймера помещаются в очередь по очереди), и микрозадача будет быть выполненным первым (Это то же самое, что и браузер), поэтому распечатайте обещание3
- Затем войдите в стадию таймеров, выполните функцию обратного вызова timer1, распечатайте timer1, поместите обратный вызов promise.then в очередь микрозадач, выполните timer2 в тех же шагах и напечатайте timer2; это сильно отличается от браузера.На этапе таймеров есть несколько setTimeout/setInterval, которые будут выполняться последовательно, в отличие от браузерной стороны, каждый раз при выполнении макрозадачи выполняется микрозадача (разница между Node и Event Loop браузера будет подробно описана ниже).
3. Микрозадача и макрозадача
В цикле событий на стороне узла также есть два типа асинхронных очередей: очередь макросов (макрозадач) и микроочередей (микрозадач).
- Общие макрозадачи, такие как: setTimeout, setInterval, setImmediate, сценарий (общий код), операции ввода-вывода и т. д.
- Общие микрозадачи, такие как: process.nextTick, new Promise().then(callback) и т. д.
4. Обратите внимание
(1) setTimeout и setImmediate
Они очень похожи, разница в основном во времени звонка.
- Дизайн setImmediate выполняется, когда фаза опроса завершена, то есть фаза проверки;
- setTimeout предназначен для выполнения, когда стадия опроса простаивает и достигнуто установленное время, но выполняется на стадии таймера.
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
- Для приведенного выше кода setTimeout может выполняться до или после.
- Первый setTimeout(fn, 0) === setTimeout(fn, 1), который определяется исходным кодом Вход в цикл событий тоже стоит денег, если на подготовку уходит больше 1 мс, callback setTimeout будет выполняться прямо в фазе таймера.
- Если время подготовки занимает менее 1 мс, то сначала выполняется обратный вызов setImmediate
Но когда оба вызываются внутри асинхронного обратного вызова ввода-вывода, setImmediate всегда выполняется первым, а затем setTimeout.
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// immediate
// timeout
В приведенном выше коде setImmediate всегда выполняется первым. Поскольку два кода записаны в обратном вызове ввода-вывода, обратный вызов ввода-вывода выполняется на этапе опроса.Когда обратный вызов выполняется, очередь пуста, и обнаруживается, что есть обратный вызов setImmediate, поэтому он сразу переходит к проверке фаза для выполнения обратного вызова.
(2) process.nextTick
Эта функция на самом деле не зависит от Event Loop, у нее есть своя очередь, и по завершении каждого этапа, если есть очередь nextTick, она очистит все callback-функции в очереди и выполнит ее раньше других микрозадач.
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
Пять, различия в цикле событий узла и браузера
В среде браузера очередь задач микрозадачи выполняется после выполнения каждой макрозадачи. В Node.js микрозадача будет выполняться между различными этапами цикла событий, то есть после выполнения этапа будут выполняться задачи очереди микрозадач..
Далее мы используем пример, чтобы проиллюстрировать разницу между ними:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Результаты работы на стороне браузера:timer1=>promise1=>timer2=>promise2
Процесс обработки на стороне браузера выглядит следующим образом:
Есть два случая запуска результатов на стороне узла:
- Если версия node11 выполняет задачу макроса (setTimeout, setInterval и setImmediate) на этапе, очередь микрозадач выполняется немедленно, что согласуется с операцией на стороне браузера, и окончательный результат
timer1=>promise1=>timer2=>promise2
- Если это node10 и его предыдущая версия: это зависит от того, выполняется ли первый таймер и находится ли второй таймер в очереди завершения.
- Если второй таймер еще не находится в очереди завершения, окончательный результат
timer1=>promise1=>timer2=>promise2
- Если второй таймер уже находится в очереди завершения, окончательный результат
timer1=>timer2=>promise1=>promise2
(Следующая процедура объясняется на основе этого случая)
- Если второй таймер еще не находится в очереди завершения, окончательный результат
1. Выполняется глобальный скрипт (main()) и два таймера помещаются в очередь таймеров по очереди.После выполнения main() стек вызовов освобождается и начинает выполняться очередь задач;
2. Сначала войдите в стадию таймеров, выполните функцию обратного вызова timer1, напечатайте timer1 и поместите обратный вызов promise1.then в очередь микрозадач, выполните timer2 в тех же шагах и напечатайте timer2;
3. В этот момент выполнение этапа таймера заканчивается, и перед тем, как цикл обработки событий перейдет на следующий этап, выполняются все задачи в очереди микрозадач, и поочередно печатаются обещания1 и обещания2.
Процесс обработки на стороне узла выглядит следующим образом:
6. Резюме
В среде браузера и Node время выполнения очереди микрозадач отличается.
- На стороне Node микрозадачи выполняются между этапами цикла событий.
- На стороне браузера микрозадача выполняется после выполнения макрозадачи цикла событий
постскриптум
Статья была опубликована в ночь на 2019.1.16 и повторно отредактировала результат запуска последнего примера на узле! Отдельное спасибо еще разzy445566отличный обзор,Поскольку версия узла обновлена до 11, принцип работы Event Loop изменился: после выполнения макрозадачи (setTimeout, setInterval и setImmediate) на этапе немедленно выполняется очередь микрозадач, что согласуется со стороной браузера. ..
Справочная статья
- процесс браузера? нить? тупо не могу сказать!
- Эти вещи о механизме цикла событий
- Принцип и практика оптимизации производительности интерфейса
- предварительное интервью
- Глубокое понимание механизма цикла событий js (Node.js)
- Подробно объясните механизм цикла событий в JavaScript.
- event-loop-timers-and-nexttick
- timers: run nextTicks after each immediate and timer