Глубокое понимание механизма цикла обработки событий NodeJS.

Node.js

Управляемое чтение

ВСЕ ВРЕМЯ, большую часть того, что мы пишемjavascriptКод компилируется и запускается в среде браузера, поэтому, возможно, мы знаем больше о механизме цикла событий браузера, чемNode.JSЦикл событий является более подробным, но когда я недавно начал подробно изучать NodeJS, я обнаружил, что механизм цикла событий NodeJS сильно отличается от браузера. мои друзья забывают.Читать и понимать позже.

Использованная литература:

在这里插入图片描述

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

Прежде всего, нам нужно понять некоторые из самых основных вещей, таких как этот цикл событий. Цикл событий относится к Node.js, выполняющему неблокирующие операции ввода-вывода. Хотя ==JavaScript является однопоточным==, потому что большинство ядер == являются многопоточными ==,Node.jsПо возможности операции загружаются в ядро ​​системы. Таким образом, они могут обрабатывать несколько операций, выполняемых в фоновом режиме. Когда одна из этих операций завершается, ядро ​​сообщаетNode.js,так чтоNode.jsСоответствующие обратные вызовы могут быть добавлены в очередь опроса для возможного выполнения.

Инициализируется при запуске Node.jsevent loop, Каждыйevent loopОба содержат шесть этапов цикла в следующем порядке:

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
  • 1. timersсцена: Эта фаза выполняетсяsetTimeout(callback)а такжеsetInterval(callback)обратный звонок по расписанию;
  • 2. I/O callbacksсцена: Обратный вызов для этой фазы для выполнения некоторых системных операций, таких как тип ошибки TCP. Например, некоторые системы *nix хотят подождать, чтобы сообщить об ошибке, если сокет TCP получает ECONNREFUSED при попытке подключения. Это будет ожидать выполнения операции в фазе обратного вызова ==I/O==;
  • 3. idle, prepareсцена: используется только внутри узла;
  • 4. pollсцена: получать новые события ввода-вывода, такие как операции чтения файлов и т. д., узел будет блокироваться здесь при соответствующих условиях;
  • 5. checkсцена: воплощать в жизньsetImmediate()установить обратные вызовы;
  • 6. close callbacksсцена: Напримерsocket.on(‘close’, callback)Обратный вызов будет выполнен на этом этапе;

Сведения о цикле событий

在这里插入图片描述
Эта диаграмма является принципом работы всего Node.js.Слева направо, сверху вниз Node.js делится на четыре слоя, а именно应用层,V8引擎层,Node API层а такжеLIBUV层.

  • Прикладной уровень: уровень взаимодействия с JavaScript, распространенными являются модули Node.js, такие как http, fs
  • Слой движка V8: то есть используйте движок V8 для анализа грамматики JavaScript, а затем взаимодействуйте с API более низкого уровня.
  • Слой NodeAPI: обеспечивает системные вызовы для модулей верхнего уровня, обычно реализованных на языке C, и взаимодействует с операционной системой.
  • Слой LIBUV: это кроссплатформенная нижняя инкапсуляция, которая реализует циклы событий, операции с файлами и т. д. и является ядром асинхронной реализации Node.js.

Подробное объяснение содержания каждой стадии цикла

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

  • Примечание. Технически фаза опроса контролирует время выполнения таймеров.

  • Примечание. Это нижнее предельное время имеет диапазон: [1, 2147483647], если установленное время не находится в этом диапазоне, оно будет установлено на 1.

I/O callbacksсценаНа этом этапе выполняются обратные вызовы для некоторых системных операций. Такие как ошибки TCP, такие как сокет TCP, получающий ECONNREFUSED при попытке подключения, Unix-подобные системы ждут, чтобы сообщить об ошибках, которые ставятся в очередь на этапе обратных вызовов ввода-вывода для выполнения. Имя может ввести в заблуждение при выполнении обработчика обратного вызова ввода-вывода, на самом деле обратный вызов ввода-вывода обрабатывается фазой опроса.

pollсценаФаза опроса выполняет две основные функции: (1) выполняет обратный вызов таймеров, чье нижнее предельное время достигнуто, (2) затем обрабатывает события в очереди опроса. Когда цикл обработки событий переходит в фазу опроса, а таймеры не запланированы (нет запланированных таймеров), произойдет одно из двух:

  • Если очередь опроса не пуста, цикл обработки событий будет проходить по очереди и синхронно выполнять обратный вызов до тех пор, пока очередь не будет опустошена или количество выполненных обратных вызовов не достигнет верхнего системного предела;

  • Если очередь опроса пуста, происходит одно из двух:

    • Если код установил обратный вызов с помощью setImmediate(), цикл событий завершит фазу опроса и перейдет в фазу проверки для выполнения очереди проверки (обратный вызов внутри).
    • Если в коде нет обратного вызова, установленного setImmediate(), цикл обработки событий заблокируется на этом этапе, ожидая добавления обратного вызова в очередь опроса и немедленного его выполнения.
  • Однако, когда цикл событий входит в фазу опроса и установлены таймеры, как только очередь опроса пуста (фаза опроса простаивает): Цикл событий будет проверять таймеры, если достигнуто нижнее предельное время 1 или более таймеров, цикл событий перейдет к этапу таймеров и выполнит очередь таймеров.

checkсценаЭта фаза позволяет выполнять обратные вызовы сразу после завершения фазы опроса. Если фаза опроса простаивает и есть обратный вызов, установленный setImmediate(), цикл событий перейдет к фазе проверки вместо ожидания.

  • setImmediate() на самом деле является специальным таймером, который запускается на отдельном этапе цикла обработки событий. оно используетlibuvAPI чтобы настроить обратный вызов, который будет выполняться сразу после окончания фазы опроса.

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

close callbacksсценаЕсли сокет или дескриптор внезапно закрываются (например, socket.destroy()), на этом этапе будет запущено событие закрытия, в противном случае оно будет запущено через process.nextTick().

在这里插入图片描述
Здесь давайте проиллюстрируем этот процесс с помощью псевдокода:

// 事件循环本身相当于一个死循环,当代码开始执行的时候,事件循环就已经启动了
// 然后顺序调用不同阶段的方法
while(true){
// timer阶段
	timer()
// I/O callbacks阶段
	IO()
// idle阶段
	IDLE()
// poll阶段
	poll()
// check阶段
	check()
// close阶段
	close()
}
// 在一次循环中,当事件循环进入到某一阶段,加入进入到check阶段,突然timer阶段的事件就绪,也会等到当前这次循环结束,再去执行对应的timer阶段的回调函数 
// 下面看这里例子
const fs = require('fs')

// timers阶段
const startTime = Date.now();
setTimeout(() => {
    const endTime = Date.now()
    console.log(`timers: ${endTime - startTime}`)
}, 1000)

// poll阶段(等待新的事件出现)
const readFileStart =  Date.now();
fs.readFile('./Demo.txt', (err, data) => {
    if (err) throw err
    let endTime = Date.now()
    // 获取文件读取的时间
    console.log(`read time: ${endTime - readFileStart}`)
    // 通过while循环将fs回调强制阻塞5000s
    while(endTime - readFileStart < 5000){
        endTime = Date.now()
    }

})


// check阶段
setImmediate(() => {
    console.log('check阶段')
})
/*控制台打印
check阶段
read time: 9
timers: 5008
通过上述结果进行分析,
1.代码执行到定时器setTimeOut,目前timers阶段对应的事件列表为空,在1000s后才会放入事件
2.事件循环进入到poll阶段,开始不断的轮询监听事件
3.fs模块异步执行,根据文件大小,可能执行时间长短不同,这里我使用的小文件,事件大概在9s左右
4.setImmediate执行,poll阶段暂时未监测到事件,发现有setImmediate函数,跳转到check阶段执行check阶段事件(打印check阶段),第一次时间循环结束,开始下一轮事件循环
5.因为时间仍未到定时器截止时间,所以事件循环有一次进入到poll阶段,进行轮询
6.读取文件完毕,fs产生了一个事件进入到poll阶段的事件队列,此时事件队列准备执行callback,所以会打印(read time: 9),人工阻塞了5s,虽然此时timer定时器事件已经被添加,但是因为这一阶段的事件循环为完成,所以不会被执行,(如果这里是死循环,那么定时器代码永远无法执行)
7.fs回调阻塞5s后,当前事件循环结束,进入到下一轮事件循环,发现timer事件队列有事件,所以开始执行 打印timers: 5008

ps:
1.将定时器延迟时间改为5ms的时候,小于文件读取时间,那么就会先监听到timers阶段有事件进入,从而进入到timers阶段执行,执行完毕继续进行事件循环
check阶段
timers: 6
read time: 5008
2.将定时器事件设置为0ms,会在进入到poll阶段的时候发现timers阶段已经有callback,那么会直接执行,然后执行完毕在下一阶段循环,执行check阶段,poll队列的回调函数
timers: 2
check阶段
read time: 7
 */

В анализ дела

Давайте посмотрим на простойEventLoopпример:

const fs = require('fs');
let counts = 0;

// 定义一个 wait 方法
function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

// 读取本地文件 操作IO
function asyncOperation (callback) {
  fs.readFile(__dirname + '/' + __filename, callback);
}

const lastTime = Date.now();

// setTimeout
setTimeout(() => {
  console.log('timers', Date.now() - lastTime + 'ms');
}, 0);

// process.nextTick
process.nextTick(() => {
  // 进入event loop
  // timers阶段之前执行
  wait(20);
  asyncOperation(() => {
    console.log('poll');
  });  
});

/**
 * timers 21ms
 * poll
 */

Вот, чтобы сделать этоsetTimeoutимеет приоритет надfs.readFileобратный вызов, выполненныйprocess.nextTick, указывая на то, что вводtimersперед этапом, подожди20msЗатем выполните чтение файла.

1. nextTickа такжеsetImmediate

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

  • setImmediateОбратный вызов находится в фазе проверки. Когда очередь в фазе опроса пуста, а очередь событий в фазе проверки существует, он переключается на фазу проверки для выполнения.

Опасности рекурсии nextTick

Поскольку nextTick имеет механизм сокращения очереди, рекурсия nextTick предотвратит переход механизма цикла событий на следующую стадию.В результате обработка ввода-вывода завершена или запланированная задача все еще не может быть выполнена, что приводит к голоданию других обработчики событий Чтобы предотвратить рекурсию Проблема, Node.js предоставляет process.maxTickDepth (по умолчанию 1000).

const fs = require('fs');
let counts = 0;

function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

function nextTick () {
  process.nextTick(() => {
    wait(20);
    console.log('nextTick');
    nextTick();
  });
}

const lastTime = Date.now();

setTimeout(() => {
  console.log('timers', Date.now() - lastTime + 'ms');
}, 0);

nextTick();

никогда не могу перейти кtimerэтап для выполненияsetTimeout里面的回调方法, потому что входtimersЕсть постоянные перед сценой.nextTickВставьте выполнение. Если предел выполнения не будет достигнут 1000 раз, вышеуказанный случай будет продолжать распечатыватьсяnextTickнить

2. setImmediate

если вI/O周期планирование в пределах, setImmediate() всегда будет выполняться до любых таймеров (setTimeout, setInterval).

3. setTimeoutа такжеsetImmediate

  • setImmediate() предназначен для выполнения обратного вызова сразу после окончания фазы опроса;
  • setTimeout() предназначен для выполнения обратного вызова после достижения указанного нижнего предела времени;

Без обработки ввода-вывода:

setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});

Результаты:

C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate

C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate

C:\Users\92809\Desktop\node_test>node test.js
timeout
immediate

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

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

A: Первый, кто войдет,timersЭтап, если производительность нашей машины средняя, ​​то введитеtimersсцена,1msпрошло ==(setTimeout(fn, 0) эквивалентно setTimeout(fn, 1))==, тогдаsetTimeoutОбратный вызов будет выполнен первым.

если не1ms, затем вtimersЗа этапом нижний предел времени не наступил,setTimeoutОбратный вызов не выполняется, приходит цикл событийpollНа данном этапе очередь пуста, поэтому продолжайте движение вниз, сначала выполните функцию обратного вызова setImmediate(), а затем выполните ее в следующем цикле обработки событий.setTimemoutфункция обратного вызова.

Краткое изложение проблемы: И когда мы ==выполняем код запуска==, мы вводимtimersВременная задержка на самом деле ==random==, что не является детерминированным, поэтому будет ситуация, когда порядок выполнения двух функций будет случайным.

Тогда давайте посмотрим на другой кусок кода:

var fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});

Результат печати следующий:

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

C:\Users\92809\Desktop\node_test>node test.js
immediate
timeout

# ... 省略 n 多次使用 node test.js 命令 ,结果都输出 immediate timeout

Вот почему и рандом вышеtimerНесоответствие, разберем причины:

Причины следующие:fs.readFileОбратный вызов находится вpollПоэтапное выполнение, когда выполняется его обратный вызов,pollочередь пуста иsetTimeoutВtimersОчередь с кодом на данный моментsetImmediate(), поэтому цикл событий сначала входитcheckstage выполняет обратный вызов, затем в следующем цикле событийtimersОбратный вызов выполняется на этапе.

Конечно, следующий небольшой случай такой же:

setTimeout(() => {
    setImmediate(() => {
        console.log('setImmediate');
    });
    setTimeout(() => {
        console.log('setTimeout');
    }, 0);
}, 0);

Приведенный выше код находится вtimersФаза выполнения внешняяsetTimeoutПосле обратного вызова внутреннийsetTimeoutа такжеsetImmediateПрисоединяйтесь к команде, а затем цикл событий продолжает переходить к следующему этапу, и переходит кpoll阶段найдено, когда队列为空, в это время есть код сsetImmedate(), поэтому сразу переходите кcheck阶段Выполните обратный вызов ответа (== Обратите внимание, что здесь нет обнаруженияtimers队列中是否有成员Событие нижней границы достигается, потому чтоsetImmediate()优先==). После этого во втором цикле событийtimersЗатем выполните соответствующий обратный вызов на этапе.

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

  • Если в основном модуле вызываются оба, то порядок выполнения зависит от производительности процесса, а значит у вас хороший комп, ну и конечно случайный.
  • Если ни один из них не вызывается в основном модуле (обернутом асинхронной операцией), то **setImmediate的回调永远先执行**.

4. nextTickа такжеPromise

Концепция: Для этих двух мы можем понимать их какмикрозадачи.也就是说,它其实не является частью цикла событий. Так когда же они это делают? Независимо от того, где они вызываются, они будут выполняться в конце цикла событий, в котором они находятся, до того, как цикл событий перейдет к следующему этапу цикла.

setTimeout(() => {
    console.log('timeout0');
    new Promise((resolve, reject) => { resolve('resolved') }).then(res => console.log(res));
    new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('timeout resolved')
      })
    }).then(res => console.log(res));
    process.nextTick(() => {
        console.log('nextTick1');
        process.nextTick(() => {
            console.log('nextTick2');
        });
    });
    process.nextTick(() => {
        console.log('nextTick3');
    });
    console.log('sync');
    setTimeout(() => {
        console.log('timeout2');
    }, 0);
}, 0);

Консоль печатает следующим образом:

C:\Users\92809\Desktop\node_test>node test.js
timeout0
sync
nextTick1
nextTick3
nextTick2
resolved
timeout2
timeout resolved

Самое резюме:timersнаружный слой сценического исполненияsetTimeoutобратный звонок, встречиСинхронный код выполняется первым, существует такжеtimeout0,syncВыход. встретитьprocess.nextTickа такжеPromiseЗатем войдите в очередь микрозадач, в свою очередьnextTick1,nextTick3,nextTick2,resolvedУдалите вывод из очереди после постановки в очередь. После этого в следующем цикле событийtimersэтап, исполнениеsetTimeoutвывод обратного вызоваtimeout2и микрозадачиPromiseвнутриsetTimeout, выходtimeout resolved. (это про микрозадачиnextTickприоритет надPromiseвыше)

5. Заключительный случай

Фрагмент кода 1:

setImmediate(function(){
  console.log("setImmediate");
  setImmediate(function(){
    console.log("嵌套setImmediate");
  });
  process.nextTick(function(){
    console.log("nextTick");
  })
});

/* 
	C:\Users\92809\Desktop\node_test>node test.js
	setImmediate
	nextTick
	嵌套setImmediate
*/

Разобрать:

цикл событийcheckВывод функции обратного вызова выполнения этапаsetImmediate, затем выводnextTick. вложенныйsetImmediateв следующем цикле событийcheckВложенные выходные данные обратного вызова выполнения этапаsetImmediate.

Фрагмент кода 2:

async function async1(){
    console.log('async1 start')
    await async2()
    console.log('async1 end')
  }
async function async2(){
    console.log('async2')
}
console.log('script start')
setTimeout(function(){
    console.log('setTimeout0') 
},0)  
setTimeout(function(){
    console.log('setTimeout3') 
},3)  
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
async1();
new Promise(function(resolve){
    console.log('promise1')
    resolve();
    console.log('promise2')
}).then(function(){
    console.log('promise3')
})
console.log('script end')

Результат печати:

C:\Users\92809\Desktop\node_test>node test.js
script start
async1 start
async2
promise1
promise2
script end
nextTick
promise3
async1 end
setTimeout0
setTimeout3
setImmediate

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