Микрозадачи, макрозадачи и цикл событий

Node.js внешний интерфейс WeChat JavaScript

Во-первых, JavaScript — это однопоточный язык сценариев.

То есть это означает, что во время выполнения одной строки кода не должно быть другой строки кода, которая выполняется в то же время, как и при использованииalert()сойти с ума позжеconsole.log, если всплывающее окно не закрыто, консоль не будет отображать сообщениеlogинформационный.

Либо какой-то код выполняет множество вычислений, например, фантомные операции, такие как взлом пароля методом перебора на фронтенде, из-за чего последующий код будет все время ждать, а страница находится в состоянии анабиоза, потому что предыдущая код не был выполнен.

Так что, если весь код будет выполняться синхронно, это вызовет серьезные проблемы.Например, если мы хотим получить какие-то данные с удаленного конца, должны ли мы продолжать перебирать код, чтобы определить, получен ли возвращаемый результат?Это как пойти в ресторан заказывать еду, вы точно не можете сказать, что после заказа вы пойдете на заднюю кухню призывать людей готовить, и вас побьют.

Итак, есть концепция асинхронных событий, регистрируем callback-функцию, например отправку сетевого запроса, мы говорим основной программе подождать, пока данные не будут получены, и уведомить меня, а затем мы можем заняться другими делами.

Тогда после асинхронного завершения нас уведомят, но в это время программа может заниматься другими делами, поэтому даже если асинхронное завершение будет завершено, вам нужно подождать в стороне.Когда программа простаивает, вы успеете посмотреть какой асинхронник выполнен.можно переходить к реализации.

Например, если вы берете такси, если водитель подъезжает первым, а у вас еще есть какие-то дела, то водитель не может уехать первым, и он должен дождаться, пока вы закончите и сядете в машину раньше. уход.

Разница между микрозадачами и макрозадачами

Это как пойти в банк по делам: сначала нужно получить номер и договориться о номере.
Обычно что-то вроде: «Ваш номер ХХ, впереди ХХ человек» и так далее.

Поскольку функции кассира заключаются в том, чтобы в одно и то же время иметь дело с клиентом, который приходит для ведения бизнеса, то каждого человека, который приходит для ведения бизнеса, можно рассматривать как макрозадачу банковского кассира. запуск следующей задачи макроса.
Таким образом, сочетание нескольких макрозадач можно рассматривать как очередь задач, которая содержит всех клиентов в текущем банке.
Очередь задач заполнена выполненными асинхронными операциями, не то что регистрация асинхронной задачи будет помещена в очередь задач, так же как и расстановка номера в банке.Если вас нет на месте, когда вам звонят, то ваш текущий номерной знак недействительно, кассир решит пропустить бизнес-обработку следующего клиента, и вам нужно будет снова получить номер после того, как вы вернетесь

Кроме того, во время выполнения макрозадачи вы можете добавить некоторые микрозадачи, как и при ведении бизнеса за прилавком.Пожилой человек перед вами может вносить депозит.После завершения депозитного дела кассир попросите старика заплатить Есть ли какие-либо другие дела, которые нужно решить? В это время старик подумал некоторое время: «В последнее время было много взрывов P2P. Вы хотите выбрать более стабильное финансовое управление ?» Затем он сказал кассиру, что хочет заняться кое-какими делами по управлению финансами. В это время кассир Вы определенно не можете сказать старику: «Вы можете пойти в заднюю часть и взять номер, и снова встать в очередь. ".
Итак, изначально была ваша очередь заниматься делами, но потому старик временно добавил:Финансовый бизнес"И оттолкнуться.
Может быть, старик все еще думает после того, как закончил свое финансовое управлениеПолучить другую кредитную карту? илиКупите еще несколько памятных монет?
Какими бы ни были потребности, если кассир может справиться с этим за нее, он сделает эти вещи, прежде чем заниматься вашим бизнесом, что можно рассматривать как микрозадачи.

Это указывает на то, что:Твой дядя всегда будет твоим дядей
Если текущая микрозадача не выполнена, следующая макрозадача выполняться не будет.

Итак, фрагмент кода, который часто используется в вопросах на собеседованиях и в различных блогах:

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)

setTimeoutсуществует как задача макроса, иPromise.thenЭто репрезентативная микрозадача, и порядок выполнения приведенного выше кода выводится в соответствии с серийным номером.

Вся асинхронность, которая войдет, относится к той части кода в обратном вызове события.
то естьnew PromiseКод, выполняемый в процессе создания экземпляра, выполняется синхронно, иthenОбратные вызовы, зарегистрированные в, выполняются асинхронно.
После выполнения синхронного кода вернитесь, чтобы проверить, завершена ли асинхронная задача, и выполните соответствующий обратный вызов, и микрозадача будет выполнена перед макрозадачей.
Таким образом, приведенный выше выходной вывод получается1、2、3、4.

+ Синхронное исполнение кодовой части представляет

+setTimeout(_ => {
-  console.log(4)
+})

+new Promise(resolve => {
+  resolve()
+  console.log(1)
+}).then(_ => {
-  console.log(3)
+})

+console.log(2)

изначальноsetTimeoutСначала был установлен таймер (эквивалентно взятию числа), а затем некоторые из них добавляются в текущий процесс.Promiseпроцессинг (временное добавление услуг).

Настолько продвинутый, даже если мы продолжимPromiseэкземпляр вPromise, его вывод все равно будет раньше, чемsetTimeoutЗадача макроса:

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
  Promise.resolve().then(_ => {
    console.log('before timeout')
  }).then(_ => {
    Promise.resolve().then(_ => {
      console.log('also before timeout')
    })
  })
})

console.log(2)

Конечно, на практике таких простых вызовов очень мало.PromiseДа, в нем обычно есть и другие асинхронные операции, напримерfetch,fs.readFileтакие операции.
И это фактически эквивалентно регистрации макрозадачи, а не микрозадачи.

P.S. вСпецификация Promise/A+середина,PromiseРеализация может быть как микрозадачей, так и макрозадачей, но представление общего консенсуса (по крайней мере,Chromeготово),PromiseОн должен принадлежать к лагерю микрозадач

Поэтому очень важно понимать, какие операции являются макрозадачами, а какие — микрозадачами.В отрасли популярна поговорка:

задача макроса

# браузер Node
I/O
setTimeout
setInterval
setImmediate
requestAnimationFrame

Некоторые места будут перечисленыUI Rendering, сказал, что это тоже задача макроса, но прочитавДокумент спецификации HTMLПозже выяснилось, что это явно шаг операции параллельно с микрозадачей.
requestAnimationFrameСкажем так, это макрозадача.requestAnimationFrameсуществуетОпределение MDNэто операция, выполняемая перед перерисовкой следующей страницы, и перерисовка также существует как шаг макрозадачи, и этот шаг позже, чем выполнение микрозадачи

микрозадачи

# браузер Node
process.nextTick
MutationObserver
Promise.then catch finally

Что такое цикл событий

Выше обсуждалось выполнение макрозадач, микрозадач и различных задач.
Но вернемся к реальности,JavaScriptЭто язык с одним процессом, и он не может обрабатывать несколько задач одновременно, поэтому когда вы выполняете макрозадачи, а когда — микрозадачи? Нам нужно, чтобы такая логика суждения существовала.

Каждый раз, когда бизнес завершается, кассир спрашивает текущего клиента, есть ли какие-либо другие дела, которые необходимо решить.(Проверьте, есть ли микрозадачи для работы)
После того, как клиент четко сообщил, что делать нечего, кассир пошел проверить, есть ли еще люди, ожидающие, чтобы заняться бизнесом.(Завершите эту задачу макроса и проверьте, есть ли какие-либо задачи макроса, которые нужно обработать)
Процесс этой проверки является непрерывным, и он будет выполняться один раз при каждом завершении задачи, и такая операция называетсяEvent Loop.(Это очень простое описание, на деле будет намного сложнее)

И как упоминалось выше, кассир может обрабатывать только одну вещь за раз, даже если эти вещи предлагает клиент, поэтому можно считать, что есть еще и очередь на микрозадачи, что примерно так и логично:

const macroTaskList = [
  ['task1'],
  ['task2', 'task3'],
  ['task4'],
]

for (let macroIndex = 0; macroIndex < macroTaskList.length; macroIndex++) {
  const microTaskList = macroTaskList[macroIndex]
  
  for (let microIndex = 0; microIndex < microTaskList.length; microIndex++) {
    const microTask = microTaskList[microIndex]

    // 添加一个微任务
    if (microIndex === 1) microTaskList.push('special micro task')
    
    // 执行任务
    console.log(microTask)
  }

  // 添加一个宏任务
  if (macroIndex === 2) macroTaskList.push(['special macro task'])
}

// > task1
// > task2
// > task3
// > special micro task
// > task4
// > special macro task

Причина использования двухforОн представлен петлей, потому что его очень удобно выполнять внутри петли.pushТакие операции (добавление каких-то задач), чтобы количество итераций динамически увеличивалось.

И чтобы было ясно,Event LoopОн отвечает только за то, чтобы сообщить вам, какие задачи выполнять или какие обратные вызовы запускаются, реальная логика все еще выполняется в процессе.

Производительность в браузере

Разница между этими двумя задачами кратко описана выше, иEvent LoopТак как же это выглядит в реальном браузере?
Первое, что нужно уяснить, это то, что макрозадача должна выполняться после микрозадачи (поскольку микрозадача фактически является одним из шагов макрозадачи).

I/OЭто кажется немного общим, слишком много вещей, чтобы назвать этоI/O, нажмите один разbutton, загрузку файла и взаимодействие с программой можно назватьI/O.

Допустим есть такиеDOMструктура:

<style>
  #outer {
    padding: 20px;
    background: #616161;
  }

  #inner {
    width: 100px;
    height: 100px;
    background: #757575;
  }
</style>
<div id="outer">
  <div id="inner"></div>
</div>
const $inner = document.querySelector('#inner')
const $outer = document.querySelector('#outer')

function handler () {
  console.log('click') // 直接输出

  Promise.resolve().then(_ => console.log('promise')) // 注册微任务

  setTimeout(_ => console.log('timeout')) // 注册宏任务

  requestAnimationFrame(_ => console.log('animationFrame')) // 注册宏任务

  $outer.setAttribute('data-random', Math.random()) // DOM属性修改,触发微任务
}

new MutationObserver(_ => {
  console.log('observer')
}).observe($outer, {
  attributes: true
})

$inner.addEventListener('click', handler)
$outer.addEventListener('click', handler)

Если вы нажмете#inner, порядок выполнения должен быть:click -> promise -> observer -> click -> promise -> observer -> animationFrame -> animationFrame -> timeout -> timeout.

потому что однаждыI/OСоздал задачу макроса, а это значит, что она будет срабатывать в этой задачеhandler.
Судя по комментариям в коде, после выполнения синхронизируемого кода пора проверить, есть ли микрозадачи, которые можно выполнить, а затем найтиPromiseа такжеMutationObserverВыполняются две микрозадачи.
потому чтоclickСобытия будут пузыриться, так что на этот раз соответствующиеI/Oсработает дваждыhandlerфункция(однажды вinner, однажды вouter), поэтому всплывающее событие будет выполнено первым (Перед другими макрозадачами), что означает, что описанная выше логика будет повторяться.
После выполнения кода синхронизации и микрозадач продолжайте поиск любых макрозадач в обратном направлении.
Следует отметить, что, поскольку мы запускаемsetAttribute, который фактически изменяетDOMсвойство, которое вызывает перерисовку страницы, и этоsetоперации выполняются синхронно, т.requestAnimationFrameОбратный вызов будет раньше, чемsetTimeoutвыполнено.

несколько маленьких сюрпризов

Используя приведенный выше пример кода, если вы вручную нажметеDOMМетод триггера элемента становится$inner.click(), то будут получены разные результаты.
существуетChromeПоследовательность вывода ниже примерно следующая:
click -> click -> promise -> observer -> promise -> animationFrame -> animationFrame -> timeout -> timeout.

Триггер вручную с намиclickПричина разного порядка выполнения заключается в том, что не триггерное событие реализуется пользователем при нажатии на элемент, аdispatchEventЯ не думаю, что этот подход эффективен.I/Oпосле выполнения один разhandlerОбратный вызов зарегистрировал микрозадачу, после регистрации макрозадачи, фактически снаружи$inner.click()не закончено.
Следовательно, прежде чем микрозадача будет выполнена, она должна продолжать всплывать, чтобы выполнить следующее событие, то есть сработать второе событие.handler.
Так выведите второй разclick, подождите, пока эти два разаhandlerПосле того, как все выполнения будут завершены, он проверит, есть ли микрозадачи и есть ли макрозадачи.

Два момента, на которые следует обратить внимание:

  1. .click()Этот способ запуска события лично думает, что он похож наdispatchEvent, что можно понимать как код, выполняемый синхронно
document.body.addEventListener('click', _ => console.log('click'))

document.body.click()
document.body.dispatchEvent(new Event('click'))
console.log('done')

// > click
// > click
// > done
  1. MutationObserverСлушатель не скажет, что он срабатывает несколько раз одновременно, и только один обратный вызов будет срабатывать для нескольких модификаций.
new MutationObserver(_ => {
  console.log('observer')
  // 如果在这输出DOM的data-random属性,必然是最后一次的值,不解释了
}).observe(document.body, {
  attributes: true
})

document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())
document.body.setAttribute('data-random', Math.random())

// 只会输出一次 ovserver

Это как пойти в ресторан, чтобы заказать еду, а официант трижды крикнул: говяжья лапша номер ХХ, это не значит, что она даст вам три тарелки говяжьей лапши.
Вышеперечисленные точки зрения относятся кTasks, microtasks, queues and schedules, в тексте есть анимированная версия пояснения

Производительность в узле

Node тоже однопоточный, но обработкаEvent LoopОн немного отличается от браузера, вотОфициальная документация узлаадрес г.

Что касается уровня API, Node добавил два новых метода, которые можно использовать:process.nextTickи макро задачиsetImmediate.

Разница между setImmediate и setTimeout

Как указано в официальной документации,setImmediateоднаждыEvent LoopВызывается после завершения выполнения.
setTimeoutОн выполняется путем вычисления времени задержки.

Но также упоминается, что если эти две операции выполняются непосредственно в основном процессе, трудно гарантировать, какая из них сработает первой.
Потому что если в основном процессе сначала регистрируются две задачи, то код для выполнения занимает больше времени, чемXXs, а таймер уже находится в состоянии, когда обратный вызов может быть выполнен.
Таким образом, таймер будет выполняться первым, а после того, как таймер будет выполнен, он завершится один раз.Event Loop, то он будет выполнятьсяsetImmediate.

setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))

Если вам интересно, вы можете попробовать это сами, и вы получите разные результаты, если будете выполнять его несколько раз.

Но после последующего добавления некоторых кода вы можете гарантироватьsetTimeoutБудет вsetImmediateЗапущено до:

setTimeout(_ => console.log('setTimeout'))
setImmediate(_ => console.log('setImmediate'))

let countdown = 1e9

while(countdown--) { } // 我们确保这个循环的执行速度会超过定时器的倒计时,导致这轮循环没有结束时,setTimeout已经可以执行回调了,所以会先执行`setTimeout`再结束这一轮循环,也就是说开始执行`setImmediate`

Если в другой задаче макроса, он должен бытьsetImmediateВыполнить сначала:

require('fs').readFile(__dirname, _ => {
  setTimeout(_ => console.log('timeout'))
  setImmediate(_ => console.log('immediate'))
})

// 如果使用一个设置了延迟的setTimeout也可以实现相同的效果

process.nextTick

Как упоминалось выше, это можно рассматривать какPromiseа такжеMutationObserverреализация микрозадач, которые можно вставлять в любой момент выполнения кодаnextTick, и гарантированно будет выполнен до запуска следующей задачи макроса.

Одним из наиболее распространенных примеров с точки зрения использования являются операции некоторых классов привязки событий:

class Lib extends require('events').EventEmitter {
  constructor () {
    super()

    this.emit('init')
  }
}

const lib = new Lib()

lib.on('init', _ => {
  // 这里将永远不会执行
  console.log('init!')
})

Поскольку приведенный выше код создает экземплярLibОбъект выполняется синхронно и отправляется сразу после завершения создания экземпляра.initмероприятие.
В это время основная программа во внешнем слое еще не начала выполняться.lib.on('init')Этот шаг прослушивания событий.
Поэтому при отправке события обратного вызова не будет, и событие не будет отправлено повторно после регистрации обратного вызова.

Мы можем легко использоватьprocess.nextTickДля решения этой проблемы:

class Lib extends require('events').EventEmitter {
  constructor () {
    super()

    process.nextTick(_ => {
      this.emit('init')
    })

    // 同理使用其他的微任务
    // 比如Promise.resolve().then(_ => this.emit('init'))
    // 也可以实现相同的效果
  }
}

Это сработает, когда программа простаивает после выполнения кода основного процесса.Event LoopПроцесс находит, есть ли микрозадачи, а затем отправляет ихinitмероприятие.

Как упоминалось в некоторых статьях, цикл вызываетprocess.nextTickЭто вызвало сигнал тревоги, а последующий код никогда не будет выполнен, что правильно, см. Реализацию двойного петля, используемого вышеloop, что эквивалентно каждый разforМассив выполняется в цикле выполненияpushоперация, чтобы цикл никогда не заканчивался

Несколько слов об асинхронных/ожидающих функциях

потому что,async/awaitв основном на основеPromiseнекоторые пакеты, в то время какPromiseЭто своего рода микрозадача. Итак, используяawaitключевое слово сPromise.thenЭффект аналогичен:

setTimeout(_ => console.log(4))

async function main() {
  console.log(1)
  await Promise.resolve()
  console.log(3)
}

main()

console.log(2)

Код асинхронной функции перед ожиданием выполняется синхронно, что можно понимать как код перед ожиданием, принадлежащийnew PromiseКогда код передается, весь код после ожидания находится вPromise.thenобратный вызов в

подраздел

В Интернете написано много статей о механизме выполнения кода в JavaScript, я слишком поверхностен, поэтому могу лишь кратко рассказать о своем понимании этого механизма.
Я не стал переходить к сырым документам, а перечислил их пошагово, вроде просмотра текущего стека, выполнения выбранной очереди задач и разных балабал.
Я чувствую, что на самом деле писать код не очень полезно, лучше просто войти в дверь, просканировать слепоту и иметь общее представление о том, что это такое.

Рекомендуемые статьи к прочтению:

One more things

Команда Blued front-end/Node набирает сотрудников. . Неполная средняя школа и средняя школа имеют HC
Координаты Чаоян Шуанцзин, имперской столицы, если интересно, свяжитесь со мной:
чат: github_jiasm
Почта:jiashunming@blued.com

Добро пожаловать, чтобы оставить свое резюме