Различные циклы событий браузера и узла (Event Loop)

Node.js внешний интерфейс JavaScript браузер

Уведомление

В версии Node 11 цикл событий Node стал таким же, как в браузере.

задний план

Цикл событий также является распространенной темой в js. В конце февраля, после прочтения «Подробного объяснения таймеров узла» учителя Жуана Ифэна, я обнаружил, что не могу полностью протестировать механизм выполнения циклов событий js, который я видел раньше.Я также проверил некоторые другие материалы, сделал заметки, неуместным, и обобщил его в документ.

Цикл событий и механизм выполнения в браузере отличаются от таковых в узле, и их не следует путать.браузерEvent loopэто спецификация, определенная в HTML5, а в узлеlibuvреализация библиотеки. В то же время, когда я читал книгу «Введение в NodeJs», я обнаружил, что механизм узлов в то время был другим, поэтому часть этой статьи о узлах относится к версии, когда эта статья была опубликована. Настоятельно рекомендуется прочитать первые три статьи по ссылке.

среда браузера

js выполняется как один поток (без учета веб-воркеров), а весь код выполняется в стеке вызовов основного потока. Когда задача основного потока очищается, она будет опрашивать задачи в очереди задач.

очередь задач

Асинхронные задачи делятся на две категории: задачи (макрозадачи, также известные как макрозадачи) и микрозадачи (микрозадачи). Когда условия выполнения соблюдены, задача и микрозадача помещаются в соответствующие очереди для помещения в основной поток для выполнения.Мы называем эти две очереди Очередь задач (также называемая очередью макрозадач) и Очередь микрозадач.

  • Задача: Код в сценарии, Settimeate, Setinterval, I / O, UI Render.
  • микрозадача: обещание, Object.observe, MutationObserver.

Конкретный процесс

  1. Завершите выполнение задач в основном потоке выполнения.
  2. Выньте выполнение задачи из Очереди микрозадач, пока она не опустеет.
  3. Избавьтесь от очереди макрозадачОдинвыполнение задачи.
  4. Выньте выполнение задачи из Очереди микрозадач, пока она не опустеет.
  5. Повторите 3 и 4.

То есть синхронное завершение, одна макрозадача, все микрозадачи, одна макрозадача, все микрозадачи...

Уведомление

  • На странице браузера можно считать, что в начальном потоке выполнения нет кода, а код в каждом теге скрипта является самостоятельной задачей, то есть будет выполняться микрозадача, созданная в предыдущем скрипте, а затем код синхронизации в следующем сценарии будет выполнено.
  • Если микрозадача была добавлена, она продолжит выполнение микрозадачи и «застряла» в макрозадаче.
  • В некоторых версиях браузеров порядок выполнения не соответствует вышеуказанному, возможно, он не соответствует стандарту или JS и HTML часть стандартного конфликта. Вы можете прочитать первую из справочных статей.
  • new Promise((resolve, reject) =>{console.log(‘同步’);resolve()}).then(() => {console.log('异步')}),Прямо сейчасpromiseизthenа такжеcatchЭто микрозадача, а не собственный внутренний код.
  • Отдельные API для конкретных браузеров не указаны.

Поддельный код

while (true) {
  宏任务队列.shift()
  微任务队列全部任务()
}

среда узла

js выполняется как один поток, и весь код выполняется в стеке вызовов основного потока. Когда задача основного потока очищается, она будет опрашивать задачи в очереди задач.

стадия цикла

события в узлекаждый раундцикл согласнозаказРазделено на 6 этапов, реализация от libuv:

  1. таймеры: выполнить обратные вызовы setTimeout и setInterval, соответствующие условиям.
  2. Обратные вызовы ввода-вывода: существуют ли функции обратного вызова для завершенных операций ввода-вывода, остаток опроса от предыдущего раунда.
  3. бездействие, подготовка: можно игнорировать
  4. poll: Ожидание событий ввода-вывода, которые еще не завершились, ожидание закончится из-за таймеров и тайм-аутов.
  5. check: выполнить обратный вызов setImmediate.
  6. обратные вызовы close: закрыть все дескрипторы закрытия, некоторые события onclose.

исполнительный механизм

несколько очередей

В дополнение к типам задач в приведенной выше фазе цикла у нас остаются микрозадачи, общие для браузеров и узлов и уникальные для узла.process.nextTick, которые мы называем очередью микрозадач и очередью NextTick.

Мы также называем очереди выполнения нескольких этапов в цикле «Очередь таймеров», «Очередь ввода-вывода», «Очередь проверки» и «Очередь закрытия».

перед циклом

Перед входом в первый цикл будут выполнены следующие операции:

  • Синхронизировать задачу
  • сделать асинхронный запрос
  • Запланируйте время, когда таймер вступит в силу
  • воплощать в жизньprocess.nextTick()

начать цикл

Согласно 6 этапам нашего цикла, он выполняется последовательно, каждый раз все задачи на текущем этапе извлекаются и выполняются, очередь NextTick очищается, а очередь микрозадач очищается. Затем выполнить следующий этап, после выполнения всех 6 этапов войти в следующий цикл. который:

  • Очистите очередь таймеров в текущем цикле, очистите очередь NextTick и очистите очередь микрозадач.
  • Очистите очередь ввода-вывода в текущем цикле, очистите очередь NextTick и очистите очередь микрозадач.
  • Очистите очередь проверки в текущем цикле, очистите очередь NextTick и очистите очередь микрозадач.
  • Очистите очередь закрытия в текущем цикле, очистите очередь NextTick и очистите очередь микрозадач.
  • Введите следующий цикл.

Как можно заметить,nextTickсоотношение приоритетовpromiseДождитесь, пока микрозадача станет высокой.setTimeoutа такжеsetIntervalсоотношение приоритетовsetImmediateвысоко.

Уведомление

  • Если создается, когда выполняется фаза таймеровsetImmediateОн будет выполнен на этапе проверки этого цикла циклов, если он создан на этапе таймеров.setTimeout, так как таймеры были удалены, он войдет в следующий цикл, и то же самое верно для создания задачи таймеров на этапе проверки.
  • setTimeoutсоотношение приоритетовsetImmediateвысокая, но из-заsetTimeout(fn,0)Реальная задержка не может быть ровно 0 секунд, может показаться, что первая созданнаяsetTimeout(fn,0)чемsetImmediateВыполняется после обратного вызова.

Поддельный код

while (true) {
  loop.forEach((阶段) => {
    阶段全部任务()
    nextTick全部任务()
    microTask全部任务()
  })
  loop = loop.next
}

тестовый код

function sleep(time) {
  let startTime = new Date()
  while (new Date() - startTime < time) {}
  console.log('1s over')
}
setTimeout(() => {
  console.log('setTimeout - 1')
  setTimeout(() => {
      console.log('setTimeout - 1 - 1')
      sleep(1000)
  })
  new Promise(resolve => resolve()).then(() => {
      console.log('setTimeout - 1 - then')
      new Promise(resolve => resolve()).then(() => {
          console.log('setTimeout - 1 - then - then')
      })
  })
  sleep(1000)
})

setTimeout(() => {
  console.log('setTimeout - 2')
  setTimeout(() => {
      console.log('setTimeout - 2 - 1')
      sleep(1000)
  })
  new Promise(resolve => resolve()).then(() => {
      console.log('setTimeout - 2 - then')
      new Promise(resolve => resolve()).then(() => {
          console.log('setTimeout - 2 - then - then')
      })
  })
  sleep(1000)
})
  • Вывод браузера:
    setTimeout - 1 //1为单个task
    1s over
    setTimeout - 1 - then
    setTimeout - 1 - then - then 
    setTimeout - 2 //2为单个task
    1s over
    setTimeout - 2 - then
    setTimeout - 2 - then - then
    setTimeout - 1 - 1
    1s over
    setTimeout - 2 - 1
    1s over
    
  • вывод узла:
    setTimeout - 1 
    1s over
    setTimeout - 2 //1、2为单阶段task
    1s over
    setTimeout - 1 - then
    setTimeout - 2 - then
    setTimeout - 1 - then - then
    setTimeout - 2 - then - then
    setTimeout - 1 - 1
    1s over
    setTimeout - 2 - 1
    1s over
    

Также видно, что цикл обработки событий отличается в браузере и узле.

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

<!--执行完主执行线程中的任务。-->
<!--取出Microtask Queue中任务执行直到清空。-->
<!--取出Macrotask Queue中一个任务执行。-->
<!--取出Microtask Queue中任务执行直到清空。-->
<!--重复3和4。-->
以 IQ 代指微任务队列,AQ 代指宏任务队列
1. 执行完主线程中任务:主执行线程执行完毕,setTimeout-1、setTimeout-2 进入等待
2. 清空 IQ:此时 IQ 中无任务
2. 执行 AQ 中一个任务: setTimeout-1 到时间后进入 AQ 中,被执行,执行过程中 setTimeout-1-1 进入等待状态,setTimeout-1-then 直接进入 IQ 队列,由于 setTimeout-1 中有 1s 等待,此时 setTimeout-2 肯定已经进入 AQ,setTimeout-1-1 也随后进入 AQ,此时结束状态为 IQ: [setTimeout-1-then],AQ: [setTimeout-2, setTimeout-1-1]
3. 清空 IQ: 此时 IQ 中有 setTimeout-1-then,执行 setTimeout-1-then,执行过程中,setTimout-1-then-then 直接被加入 IQ,所以 IQ 没清空,所以继续执行 setTimout-1-then-then,IQ 被清空,此时结束状态为 IQ: [], AQ:  [setTimeout-2, setTimeout-1-1]
4. 执行 AQ 中一个任务:即执行 setTimeout-2
5. 清空 IQ: 这一步与 3 相似,所以输出 setTimeout-2-then、setTimeout-2-then-then,IQ 清空,此时结束状态为 IQ: [], AQ: [setTimeout-1-1, setTimeout-2-1]
6. 执行 AQ 中一个任务:即 setTimeout-1-1
7. 清空 IQ: 本身就为空
8. 执行 AQ 中一个任务:即 setTimeout-2-1

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