предисловие
Event Loop
то есть цикл событий, что означает браузер илиNode
решениеjavaScript
Механизм, который не блокируется при работе на одном потоке, то есть мы часто используемасинхронныйпринцип.
Зачем понимать цикл событий
-
Это увеличение глубины собственной технологии, то есть понимание
JavaScript
рабочий механизм. -
Сейчас в сфере фронтенда одна за другой появляются различные технологии, и овладение лежащими в их основе принципами позволяет сделать себя неизменным и реагировать на изменения.
-
Отвечайте на интервью крупных интернет-компаний, разбирайтесь в принципах и позволяйте вопросам играть.
куча, стопка, очередь
куча
кучаэто структура данных, набор данных, поддерживаемый полным бинарным деревом,кучаЕсть два типа, один самый большойкуча, а длямин куча, корневой узелмаксимумизкучаназываетсямаксимальная кучаилибольшая куча корней, корневой узелминимумизкучаназываетсямин кучаилинебольшая корневая куча.
кучадаЛинейная структура данных, эквивалентноодномерный массив, только с одним преемником.
максимальная куча
Куча
кучаВ информатике ограничивается тольконижний колонтитулпровестивставлятьилиудалятьЛинейная таблица операций.кучапредставляет собой структуру данных, которая следуетЛИФОпринцип хранения данных,войти первымданные помещаются внижняя часть стека,последние данныесуществуетвершина стека, когда вам нужно прочитать данные извершина стекаНачинатьвсплывающие данные.
кучатолько вВставьте с одного концаа такжеудалятьизспециальный линейный стол.
Очередь
Особенность в том, что он позволяет только переднюю часть таблицы (front
)провестиудалятьоперация, в то время как в бэкэнде таблицы (rear
)провестивставлятьоперация, икучаТакой же,очередьпредставляет собой линейную таблицу с ограниченными операциями.
провестивставлятьОкончание операции называетсяхвостовой конец,провестиудалятьОкончание операции называетсяглава команды. Когда в очереди нет элементов, она вызываетсяпустая очередь.
очередьЭлемент данных также называетсяэлемент очереди. Вставка элемента очереди в очередь называетсяприсоединиться к команде,оточередьсерединаудалятьЭлемент очереди называетсявне команды.因为队列Только разрешенона одном концевставлять, на другом концеудалять, так что толькосамый раннийВойтиочередьЭлементыбыть первым из очередиудалить, поэтому очередь также называетсяпервым прибыл, первым обслужен(FIFO—first in first out
)
Event Loop
существуетJavaScript
, задачи делятся на два типа, макро задача (MacroTask
)Также известен какTask
, тип микрозадачи (MicroTask
).
макрозадача
-
script
все коды,setTimeout
,setInterval
,setImmediate
(Браузер временно не поддерживает, поддерживает только IE10, см.MDN
),I/O
,UI Rendering
.
Микрозадача
-
Process.nextTick(Node独有)
,Promise
,Object.observe(废弃)
,MutationObserver
(См. конкретное использованиездесь)
Цикл событий в браузере
Javascript
есть одинmain thread
основной поток иcall-stack
Стек вызовов (стек выполнения), все задачи будут помещены в стек вызовов для ожидания выполнения основного потока.
Стек вызовов JS
В стеке вызовов JS используется правило «последняя пришла — первая вышла». Когда функция выполняется, она будет добавлена в верхнюю часть стека. Когда стек выполнения будет завершен, она будет удалена из вершины стека до тех пор, пока стек пуст.
Синхронные и асинхронные задачи
Javascript
Однопоточные задачи делятся наСинхронизировать задачуа такжеасинхронная задача, синхронная задача будет ожидать последовательного выполнения основного потока в стеке вызовов, а асинхронная задача поместит зарегистрированную функцию обратного вызова в очередь задач после того, как асинхронная задача получит результат, и будет ждать, пока основной поток будет бездействовать ( стек вызовов пуст), он считывается в стек и ожидает выполнения основного потока.
Task Queue
, то есть очередь, представляет собой структуру данных в порядке поступления.
Модель процесса цикла событий
- Выберите текущую очередь задач для выполнения и выберите первую задачу в очереди задач.Если очередь задач пуста, она будет
null
, выполнение переходит к микрозадаче (MicroTask
) этапы выполнения. - Установите задачу в цикле событий как выбранную задачу.
- выполнять задания.
- Устанавливает для текущей задачи в цикле событий значение null.
- Удалить выполненную задачу из очереди задач.
- шаг микрозадач: введите контрольную точку микрозадачи.
- Обновление рендеринга пользовательского интерфейса.
- Вернитесь к первому шагу.
Когда выполнение входит в контрольную точку микрозадачи, пользовательский агент выполняет следующие шаги:
- Установите для флага контрольной точки микрозадачи значение true.
- когда цикл событий
microtask
Когда выполнение не пустое: выберите тот, который вошел первымmicrotask
в очередиmicrotask
, который оборачивает цикл событийmicrotask
установить как выбранныйmicrotask
,бегатьmicrotask
, который будет выполненmicrotask
дляnull
, удаленныйmicrotask
серединаmicrotask
. - Очистить транзакции IndexDB
- Установите флаг входа в контрольные точки микрозадачи на false.
Вышеупомянутое может быть не легко понять.Следующее изображение-это изображение, которое я сделал.
Стек выполнения завершенСинхронизировать задачу, Посмотретьстек выполненияПуст ли он, если стек выполнения пуст, он проверитмикрозадачи(microTask
Очередь пуста, если она пуста,Task
(Макро-миссии), иначе все микроструктуры выполняются за один раз.
каждый раз одинзадача макросаПосле выполнения проверьтемикрозадачи(microTask
) пуста ли очередь, если нет, то она будет следоватьпервый пришел первыйВсе правила выполняютсямикрозадачи(microTask
), задаватьмикрозадачи(microTask
) очередьnull
, а затем выполнитьзадача макроса, и так далее.
Например
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
Сначала разделим на несколько категорий:
Первое исполнение:
Tasks:run script、 setTimeout callback
Microtasks:Promise then
JS stack: script
Log: script start、script end。
Для выполнения синхронного кода задача макроса (Tasks
) и микрозадачи (Microtasks
) в соответствующие очереди.
Второе исполнение:
Tasks:run script、 setTimeout callback
Microtasks:Promise2 then
JS stack: Promise2 callback
Log: script start、script end、promise1、promise2
После выполнения макрозадачи микрозадача (Microtasks
) Очередь не пуста, выполнитьPromise1
, выполнение завершеноPromise1
После этого позвонитеPromise2.then
, поставить микрозадачу(Microtasks
) поставить в очередь, а затем выполнитьPromise2.then
.
Третье исполнение:
Tasks:setTimeout callback
Microtasks:
JS stack: setTimeout callback
Log: script start、script end、promise1、promise2、setTimeout
Когда микрозадачи (Microtasks
) когда очередь пуста, выполнить задачу макроса (Tasks
),воплощать в жизньsetTimeout callback
, распечатайте журнал.
Четвертое исполнение:
Tasks:setTimeout callback
Microtasks:
JS stack:
Log: script start、script end、promise1、promise2、setTimeout
пустойTasksИ очередиJS stack
.
Приведенную выше анимацию кадра выполнения можно просмотретьTasks, microtasks, queues and schedules
Может быть, эта картина лучше понять.
Другой пример
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
Здесь нужно понятьasync/await
.
async/await
Преобразовывается в нижней части вpromise
а такжеthen
Перезвоните.
То есть этоpromise
синтаксический сахар.
каждый раз, когда мы используемawait
, интерпретатор создаетpromise
объект, затем поместите остальныеasync
Операции в функции помещаются вthen
в функции обратного вызова.
async/await
Реализация неотделима отPromise
. Понятный буквально,async
является сокращением от "асинхронный", аawait
даasync wait
Сокращение можно рассматривать как ожидание завершения выполнения асинхронного метода.
О разнице между версией ниже 73 и версией 73
- В старой версии выполнить сначала
promise1
а такжеpromise2
, а затем выполнитьasync1
. - В версии 73 выполнить сначала
async1
повторно выполнитьpromise1
а такжеpromise2
.
Основная причина в том, что спецификация была изменена в Google (Canary) версии 73, как показано на изображении ниже:
- Разница в том, что
RESOLVE(thenable)
разница между иPromise.resolve(thenable)
.
в старой версии
- Сначала передайте его
await
Значение заключено вPromise
середина. Затем обработчик прикрепляется к этой оберткеPromise
, так что вPromise
сталиfulfilled
Затем возобновите функцию и приостановите выполнение асинхронной функции, как толькоpromise
сталиfulfilled
, чтобы возобновить выполнение асинхронной функции. - каждый
await
Движок должен создать два дополнительных промиса (даже если правая сторона уже однаPromise
) и требуется не менее трехmicrotask
очередьticks
(tick
Это относительная единица времени системы, также известная как база времени системы, которая получается из периодического прерывания (выходного импульса) таймера.tick
, также известный как «такт часов», отметка времени. ).
Цитируя пример из Учителя Хэ Чжиху
async function f() {
await p
console.log('ok')
}
Упрощенное понимание как:
function f() {
return RESOLVE(p).then(() => {
console.log('ok')
})
}
- если
RESOLVE(p)
дляp
дляpromise
возвращаться напрямуюp
, тогдаp
изthen
Метод будет вызван немедленно, и его обратный вызов будет выполнен немедленно.job
очередь. - и если
RESOLVE(p)
Строго по стандарту он должен генерировать новыйpromise
, Хотяpromise
Конечноresolve
дляp
, но сам процесс асинхронный, который сейчас входитjob
очередь новаяpromise
изresolve
процесс, поэтомуpromise
изthen
не будет вызываться немедленно, а будет ждать, пока текущийjob
Очередь выполняется до вышеупомянутогоresolve
будет вызвана процедура, а затем ее callback (т.е. continueawait
утверждение после) перед добавлениемjob
очередь, поэтому время позднее.
Google (Canary) в версии 73
- использовать пару
PromiseResolve
призыв изменитьawait
семантика для уменьшения публичностиawaitPromise
Количество конверсий в кейсе. - если передано
await
Значение уже являетсяPromise
, то эта оптимизация позволяет избежать повторного созданияPromise
обертка, в этом случае мы начинаем как минимум с трехmicrotick
только одномуmicrotick
.
Подробный процесс:
Версии ниже 73
- Во-первых, распечатать
script start
,передачаasync1()
, возвращаетPromise
, так что распечатайтеasync2 end
. - каждый
await
, создаст новыйpromise
, но сам процесс асинхронный, поэтомуawait
Он не будет вызываться сразу после этого. - Продолжайте выполнять синхронный код, печатайте
Promise
а такжеscript end
,Будуthen
функция положитьмикрозадачипоставлен в очередь на выполнение. - После выполнения синхронизации проверьтемикрозадачиочередь
null
, а затем следуйте правилу «первым поступил — первым вышел» и последовательно выполняйте их. - затем распечатайте сначала
promise1
,В настоящее времяthen
Функция обратного вызова возвращаетundefinde
, тогда сноваthen
Прикованный вызов и положитьмикрозадачиочередь, распечатать сноваpromise2
. - назад снова
await
Место, куда возвращается выполнениеPromise
изresolve
функция, которая, в свою очередь,resolve
Закинуть в очередь микрозадачи и распечататьasync1 end
. - когдамикрозадачиКогда очередь пуста, выполните задачу макроса и распечатайте
setTimeout
.
Google (версия Canary 73)
- если передано
await
Значение уже являетсяPromise
, то эта оптимизация позволяет избежать повторного созданияPromise
обертка, в этом случае мы начинаем как минимум с трехmicrotick
только одномуmicrotick
. - Двигатель больше не нужен
await
Создайтеthrowaway Promise
- Большую часть времени. - Сейчас
promise
указывая на то жеPromise
, так что этот шаг ничего не делает. Затем двигатель продолжает работать, как и прежде, создаваяthrowaway Promise
,договариватьсяPromiseReactionJob
существуетmicrotask
Под очередьюtick
Возобновляет асинхронную функцию на , приостанавливает выполнение функции и возвращает вызывающему объекту.
Подробнее см. (здесь).
Цикл событий NodeJS
Node
серединаEvent Loop
основывается наlibuv
понял, иlibuv
даNode
Новый кроссплатформенный уровень абстракции, libuv, использует асинхронное, управляемое событиями программирование, ядром которого является обеспечениеi/o
Цикл событий и асинхронные обратные вызовы. либувAPI
Включает синхронизацию, неблокирующую сеть, асинхронные операции с файлами, дочерние процессы и многое другое.Event Loop
только что вlibuv
реализовано в.
Node
изEvent loop
Всего есть 6 этапов, каждый из которых детализирован следующим образом:
-
timers
: воплощать в жизньsetTimeout
а такжеsetInterval
средний срок годностиcallback
. -
pending callback
: меньшинство в предыдущем циклеcallback
будет выполняться на этом этапе. -
idle, prepare
: Только для внутреннего использования. -
poll
: Самый ответственный этап, выполнениеpending callback
, обратная блокировка на этом этапе при соответствующих обстоятельствах. -
check
: воплощать в жизньsetImmediate
(setImmediate()
Он заключается в том, чтобы вставить событие в хвост очереди событий и выполнить его сразу после завершения выполнения функции основного потока и очереди событий.setImmediate
указанная функция обратного вызова) изcallback
. -
close callbacks
: воплощать в жизньclose
мероприятиеcallback
,Напримерsocket.on('close'[,fn])
илиhttp.server.on('close, fn)
.
Конкретные детали заключаются в следующем:
timers
воплощать в жизньsetTimeout
а такжеsetInterval
средний срок годностиcallback
, вам нужно установить количество миллисекунд для выполнения двух обратных вызовов.Теоретически обратный вызов должен выполняться, как только время истекло, но из-заsystem
Планирование может быть задержано и не достигнуто ожидаемого времени.
Ниже приведен пример объяснения документации официального веб-сайта:
const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});
При входе в цикл событий у него пустая очередь (fs.readFile()
еще не завершено), поэтому таймер будет ждать оставшееся количество миллисекунд, и когда будет достигнуто 95 мс,fs.readFile()
Обратный вызов, который завершает чтение файла и занимает 10 мс, добавляется в очередь опроса и выполняется.
Когда обратный вызов завершается, в очереди больше нет обратных вызовов, поэтому цикл событий увидит, что достигнут самый быстрый таймер.порог, затем обратно вэтап таймеровдля выполнения обратного вызова таймера.
В этом примере вы увидите, что общая задержка между запланированным таймером и выполнением обратного вызова составит 105 мс.
Вот мои тестовые времена:
pending callbacks
На этом этапе выполняются обратные вызовы для определенных системных операций (таких как типы ошибок TCP). Например, еслиTCP socket ECONNREFUSED
Некоторые системы *nix хотят дождаться сообщений об ошибках при попытке подключения.
это будет вpending callbacks
сценическое исполнение.
poll
Этап опроса выполняет две основные функции:
- воплощать в жизнь
I/O
Перезвоните. - Обработка событий в очереди опроса.
Когда цикл события входитpoll
этапе и вtimers
Когда в файле нет исполняемого таймера, произойдет одно из двух.
- если
poll
очередь не пуста, цикл событий будет проходить по ней синхронно, чтобы выполнить ихcallback
очереди до тех пор, пока очередь не станет пустой или не достигнетsystem-dependent
(системные ограничения).
еслиpoll
очередь пуста, произойдет одно из двух
-
Если есть
setImmediate()
Обратный вызов должен быть выполнен, он немедленно остановит выполнениеpoll
этап и ввод в исполнениеcheck
этап для выполнения обратного вызова. -
если нет
setImmediate()
Вернемся к необходимости выполнить, этап опроса будет ждатьcallback
добавляется в очередь и тут же выполняется.
Конечно, если таймер установлен и очередь опроса пуста, он определит, есть ли тайм-аут таймера, и если да, то вернется на этап таймера для выполнения обратного вызова.
check
Эта фаза позволяет людям выполнять обратные вызовы, как только фаза опроса завершена.
еслиpoll
стадия простаивает иscript
в очередиsetImmediate()
, цикл событий достигает фазы проверки и выполняется вместо ожидания.
setImmediate()
На самом деле это специальный таймер, который запускается в отдельной стадии цикла событий. оно используетlibuv API
запланировать наpoll
Обратный вызов для выполнения после завершения этапа.
Как правило, когда код выполняется, цикл событий в конечном итоге достигаетpoll
этап, он будет ожидать входящие соединения, запросы и т.д.
Однако, если обратный вызов был отправленsetImmediate()
, а фаза опроса становится бездействующей, она завершается и наступаетcheck
этап вместо ожиданияpoll
мероприятие.
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')
еслиnode
Версияv11.x
,
Результат согласуется с браузером.
start
end
promise3
timer1
promise1
timer2
promise2
Для получения подробной информации см.Снова попал в цикл событий узла, на этот раз это горшок узла".
Если версия V10 выше приводит к двум случаям:
- Если таймер time2 уже находится в очереди на выполнение
start
end
promise3
timer1
timer2
promise1
promise2
- Если таймер time2 не находится в столбце пары выполнения, результат выполнения
start
end
promise3
timer1
promise1
timer2
promise2
Для получения подробной информации см.poll
Кейсы второй стадии.
Это может быть лучше понято из следующего рисунка:
Разница между setTimeout() и setImmediate()
setImmediate
а такжеsetTimeout()
Они похожи, но время их называют по-разному.
-
setImmediate()
предназначен для использования в нынешнихpoll
После завершения этапа на этапе проверки выполняется скрипт. -
setTimeout()
Сценарий, запуск которого запланирован по истечении минимума (мс) вtimers
сценическое исполнение.
Например
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
Порядок, в котором выполняются таймеры, зависит от контекста, в котором они вызываются. Если оба вызываются из основного модуля, то время будет ограничено производительностью процесса.
Результаты также противоречивы
если вI / O
Переместите два вызова в цикл, немедленный обратный вызов всегда выполняется первым:
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
Результат можно определить какimmediate => timeout
.
Основная причина в том, чтоI/O阶段
После чтения файла цикл событий сначала войдетpoll
Этап, найденныйsetImmediate
Нужно выполнить, войдет сразуcheck
сценическое исполнениеsetImmediate
Перезвоните.
затем введитеtimers
этап, исполнениеsetTimeout
,Распечататьtimeout
.
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
Process.nextTick()
process.nextTick()
Хотя он является частью асинхронного API, он не показан на схеме. Это потому чтоprocess.nextTick()
Технически это не часть цикла событий.
-
process.nextTick()
метод будетcallback
добавить вnext tick
очередь. Как только задачи в текущей очереди опроса событий будут выполнены,next tick
все в очередиcallbacks
будут вызываться последовательно.
Другой способ понимания:
- Когда каждый этап завершен, если есть
nextTick
очередь, все функции обратного вызова в очереди будут очищены, и они будут иметь приоритет над другимиmicrotask
воплощать в жизнь.
пример
let bar;
setTimeout(() => {
console.log('setTimeout');
}, 0)
setImmediate(() => {
console.log('setImmediate');
})
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1
});
bar = 1;
Может быть два ответа на выполнение приведенного выше кода в NodeV10, один из них:
bar 1
setTimeout
setImmediate
Другой:
bar 1
setImmediate
setTimeout
В любом случае, всегда выполняйте сначалаprocess.nextTick(callback)
,Распечататьbar 1
.
наконец
Спасибо @Dante_Hu за вопрос.await
Проблема со статьей исправлена.
Изменен результат выполнения на стороне узла. Разница между V10 и V11.
Чтобы узнать о проблеме с ожиданием, обратитесь к следующим статьям:.
"promise, async, await, execution order》
"Normative: Reduce the number of ticks in async/await》
"Результаты выполнения async/await в среде chrome и среде node несовместимы, решить?》
"Более быстрые асинхронные функции и обещания》
Другой упоминаемый контент:
"Механизм цикла событий браузера JS》
"Что такое цикл событий браузера (Event Loop)?》
"В статье рассказывается о цикле событий — браузере и узле.》
"Не путайте nodejs и цикл событий в браузере》
"В чем разница между браузером и циклом событий Node?》
"Tasks, microtasks, queues and schedules》
"предварительное интервью》
"Node.js представляет основные концепции 5-Libuv》
"The Node.js Event Loop, Timers, and process.nextTick()》
"официальный сайт узла》