В чем разница между браузером и циклом событий Node?

JavaScript

предисловие

В этой статье мы представим принцип асинхронной реализации в 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 для внешний мир. , механизм событийного цикла также является его реализацией (подробно описан ниже).

Механизм работы Node.js выглядит следующим образом:

  • Движок 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) на этапе немедленно выполняется очередь микрозадач, что согласуется со стороной браузера. ..

Справочная статья