Понимание цикла событий за раз (тщательно решайте такие вопросы интервью)

JavaScript
Понимание цикла событий за раз (тщательно решайте такие вопросы интервью)

предисловие

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()
"официальный сайт узла