Управляемое чтение
ВСЕ ВРЕМЯ, большую часть того, что мы пишемjavascript
Код компилируется и запускается в среде браузера, поэтому, возможно, мы знаем больше о механизме цикла событий браузера, чемNode.JS
Цикл событий является более подробным, но когда я недавно начал подробно изучать NodeJS, я обнаружил, что механизм цикла событий NodeJS сильно отличается от браузера. мои друзья забывают.Читать и понимать позже.
Использованная литература:
- Углубленный анализ цикла событий и очереди сообщений Node.js
- Порядок выполнения в Node.js (микрозадачи и цикл событий)
- Заглянуть в механизм цикла событий Node по случайному порядку выполнения setTimeout и setImmediate
Что такое цикл событий
Прежде всего, нам нужно понять некоторые из самых основных вещей, таких как этот цикл событий. Цикл событий относится к 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() на самом деле является специальным таймером, который запускается на отдельном этапе цикла обработки событий. оно использует
libuv
API чтобы настроить обратный вызов, который будет выполняться сразу после окончания фазы опроса. -
Вообще говоря, по мере выполнения кода цикл обработки событий в конечном итоге переходит в фазу опроса, где он ожидает входящих подключений, запросов и т. д. Однако пока есть обратный вызов, установленный 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()
, поэтому цикл событий сначала входитcheck
stage выполняет обратный вызов, затем в следующем цикле событий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
Как и для всех, вы можете сначала посмотреть код, изменить код молча в своем сердце, а затем сравнить результаты вывода.Конечно, последние три, я лично думаю, что это немного проблематично.В конце концов, он работает в основной модуль Ваш ответ последние три Возможны отклонения;