이 기사는 Node의 이벤트 루프에 대한 심층적인 이해를 제공할 것입니다.
항상 우리가 작성하는 대부분의 javascript
코드는 브라우저 환경에서 컴파일되고 실행되므로 이벤트 루프보다 브라우저의 이벤트 루프 메커니즘에 대해 더 많이 알고 있을 것입니다. Node.JS
의 내용이 더 심층적이지만, 최근 NodeJS를 심층적으로 연구하기 시작하면서 NodeJS의 이벤트 루프 메커니즘이 브라우저 측과 매우 다르다는 것을 알게 되었습니다. 공부하십시오. 잊어버린 후에 자신과 친구들이 읽고 이해할 수 있도록 도와주세요. javascript
代码都是在浏览器环境下编译运行的,因此可能我们对浏览器的事件循环机制了解比Node.JS
的事件循环更深入一些,但是最近写开始深入NodeJS学习的时候,发现NodeJS的事件循环机制和浏览器端有很大的区别,特此记录来深入的学习了下,以帮助自己及小伙伴们忘记后查阅及理解。
首先我们需要了解一下最基础的一些东西,比如这个事件循环,事件循环是指Node.js执行非阻塞I/O操作,尽管==JavaScript是单线程的==,但由于大多数==内核都是多线程==的,Node.js
会尽可能将操作装载到系统内核。因此它们可以处理在后台执行的多个操作。当其中一个操作完成时,内核会告诉Node.js
,以便Node.js
可以将相应的回调添加到轮询队列中以最终执行。【相关教程推荐:nodejs视频教程】
当Node.js启动时会初始化event loop
, 每一个event loop
都会包含按如下顺序六个循环阶段:
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
timers
阶段: 这个阶段执行 setTimeout(callback)
和 setInterval(callback)
预定的 callback;I/O callbacks
阶段: 此阶段执行某些系统操作的回调,例如TCP错误的类型。 例如,如果TCP套接字在尝试连接时收到 ECONNREFUSED,则某些* nix系统希望等待报告错误。 这将操作将等待在==I/O回调阶段==执行;idle, prepare
阶段: 仅node内部使用;poll
阶段: 获取新的I/O事件, 例如操作读取文件等等,适当的条件下node将阻塞在这里;check
阶段: 执行 setImmediate()
设定的callbacks;close callbacks
阶段: 比如 socket.on(‘close’, callback)
的callback会在这个阶段执行;这个图是整个 Node.js 的运行原理,从左到右,从上到下,Node.js 被分为了四层,分别是 应用层
、V8引擎层
、Node API层
和 LIBUV层
。
- 应用层: 即 JavaScript 交互层,常见的就是 Node.js 的模块,比如 http,fs
- V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互
- NodeAPI层: 为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互 。
- LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的核心 。
timers
阶段 一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。
注意:技术上来说,poll 阶段控制 timers 什么时候执行。
注意:这个下限时间有个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。
I/O callbacks
이벤트 루프란 무엇입니까🎜
🎜먼저 이벤트 루프와 같은 가장 기본적인 사항을 이해해야 합니다. 이벤트 루프는 Node.js를 참조합니다. 비차단 I/O 작업을 실행하는 경우 JavaScript는 단일 스레드이지만 대부분의 커널은 다중 스레드이므로Node.js
는 가능한 한 많은 작업을 시스템 커널에 로드합니다. 따라서 백그라운드에서 수행되는 여러 작업을 처리할 수 있습니다. 작업 중 하나가 완료되면 커널은Node.js
가 최종 실행을 위해 폴링 대기열에 해당 콜백을 추가할 수 있도록Node.js
에 지시합니다. [관련 튜토리얼 권장사항: 🎜nodejs 동영상 튜토리얼🎜]🎜🎜Node.js가 시작되면이벤트 루프
가 초기화됩니다. 각이벤트 루프
에는 다음 순서로 6개의 루프가 포함됩니다. 단계: 🎜// 事件循环本身相当于一个死循环,当代码开始执行的时候,事件循环就已经启动了 // 然后顺序调用不同阶段的方法 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: 9timers: 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: 5008ps:1.将定时器延迟时间改为5ms的时候,小于文件读取时间,那么就会先监听到timers阶段有事件进入,从而进入到timers阶段执行,执行完毕继续进行事件循环check阶段timers: 6read time: 50082.将定时器事件设置为0ms,会在进入到poll阶段的时候发现timers阶段已经有callback,那么会直接执行,然后执行完毕在下一阶段循环,执行check阶段,poll队列的回调函数timers: 2check阶段read time: 7 */
- 1.
타이머
단계 🎜: 이 단계는setTimeout(callback)
및setInterval(callback)
예약된 콜백을 실행합니다.- 2.
I/O 콜백
단계 🎜: 이 단계는 특정 시스템 작업 콜백을 수행합니다. 예를 들어 TCP 오류 유형과 같습니다. 예를 들어, 일부 *nix 시스템은 연결을 시도할 때 TCP 소켓이 ECONNREFUSED를 수신하는 경우 오류 보고를 기다리기를 원합니다. 이 작업은 ==I/O 콜백 단계==;- 3.
유휴, 준비
단계🎜: 노드에서 내부적으로만 사용됩니다.- 4.
poll
단계 🎜: 파일 읽기 작업과 같은 새로운 I/O 이벤트를 얻습니다. 적절한 조건에서 노드는 여기에서 차단됩니다. li>- 5.
check
단계 🎜:setImmediate()
콜백 설정 실행;- 6. Strong >
ul>콜백 닫기
단계🎜: 예를 들어socket.on('close', callback)
의 콜백이 이 단계에서 실행됩니다.이벤트 루프에 대한 자세한 설명🎜
🎜🎜🎜이 그림은 전체 Node.js의 작동 원리입니다. 왼쪽에서 오른쪽으로, 위에서 아래로, Node.js입니다. Node.js는애플리케이션 계층
,V8 엔진 계층
,노드 API 계층
및LIBUV 계층
의 네 가지 계층으로 나뉩니다. 🎜
- 애플리케이션 계층: 즉, JavaScript 상호 작용 계층이며 가장 일반적인 것은 http, fs와 같은 Node.js 모듈입니다.
- V8 엔진 계층: 즉, 사용 JavaScript 구문을 구문 분석한 다음 하위 API와 상호 작용하는 V8 엔진
- NodeAPI 계층: 일반적으로 C 언어로 구현되는 상위 모듈에 대한 시스템 호출을 제공하여 운영 체제와 상호 작용합니다.
- LIBUV 레이어: 이벤트 루프, 파일 작업 등을 구현하는 크로스 플랫폼 최하위 캡슐화이며 Node.js 비동기 구현의 핵심입니다.
각 주기 단계의 내용에 대한 자세한 설명🎜
🎜timersPhase 🎜 타이머는 정확한 시간 대신 하한 시간을 지정하고, 이 하한 시간에 도달한 후 콜백을 실행합니다. 타이머는 지정된 시간 이후 가능한 한 빨리 콜백을 실행하지만 시스템 예약이나 기타 콜백 실행으로 인해 콜백이 지연될 수 있습니다. 🎜
- 🎜참고: 기술적으로 말하면 폴링 단계는 타이머가 실행되는 시기를 제어합니다. 🎜
- 🎜참고: 이 하한 시간의 범위는 [1, 2147483647]입니다. 설정 시간이 이 범위를 벗어나면 1로 설정됩니다. 🎜
🎜I/O 콜백
단계 🎜 이 단계는 일부 시스템 작업에 대한 콜백을 수행합니다. 예를 들어, 연결을 시도할 때 ECONNREFUSED를 수신하는 TCP 소켓과 같은 TCP 오류, Unix 계열 시스템은 오류 보고를 기다립니다. 오류는 I/O 콜백 단계에서 대기열에 추가됩니다. 이름 때문에 I/O 콜백 핸들러가 실행되는 것으로 오해할 수 있지만 실제로는 I/O 콜백이 폴 단계에서 처리됩니다.🎜
poll
阶段 poll 阶段有两个主要功能:(1)执行下限时间已经达到的timers的回调,(2)然后处理 poll 队列里的事件。 当event loop进入 poll 阶段,并且 没有设定的 timers(there are no timers scheduled),会发生下面两件事之一:
如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;
如果 poll 队列为空,则发生以下两件事之一:
- 如果代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里面的回调 callback)。
- 如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。
但是,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态): event loop将检查timers,如果有1个或多个timers的下限时间已经到达,event loop将绕回 timers 阶段,并执行 timer 队列。
check
阶段 这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。
setImmediate() 实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用
libuv
的API 来设定在 poll 阶段结束后立即执行回调。通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。但是,只要有被setImmediate()设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。
close callbacks
阶段 如果一个 socket 或 handle 被突然关掉(比如 socket.destroy()),close事件将在这个阶段被触发,否则将通过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: 9timers: 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: 5008ps:1.将定时器延迟时间改为5ms的时候,小于文件读取时间,那么就会先监听到timers阶段有事件进入,从而进入到timers阶段执行,执行完毕继续进行事件循环check阶段timers: 6read time: 50082.将定时器事件设置为0ms,会在进入到poll阶段的时候发现timers阶段已经有callback,那么会直接执行,然后执行完毕在下一阶段循环,执行check阶段,poll队列的回调函数timers: 2check阶段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
的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行,参考nodejs进阶视频讲解:进入学习nextTick 递归的危害
由于nextTick具有插队的机制,nextTick的递归会让事件循环机制无法进入下一个阶段. 导致I/O处理完成或者定时任务超时后仍然无法执行, 导致了其它事件处理程序处于饥饿状态. 为了防止递归产生的问题, 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()被设计在 poll 阶段结束后立即执行回调;
- setTimeout()被设计在指定下限时间到达后执行回调;
无 I/O 处理情况下:
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从结果,我们可以发现,这里打印输出出来的结果,并没有什么固定的先后顺序,偏向于随机,为什么会发生这样的情况呢?
答:首先进入的是
timers
阶段,如果我们的机器性能一般,那么进入timers
阶段,1ms
已经过去了 ==(setTimeout(fn, 0)等价于setTimeout(fn, 1))==,那么setTimeout
的回调会首先执行。如果没有到
1ms
,那么在timers
阶段的时候,下限时间没到,setTimeout
回调不执行,事件循环来到了poll
阶段,这个时候队列为空,于是往下继续,先执行了setImmediate()的回调函数,之后在下一个事件循环再执行setTimemout
的回调函数。问题总结:而我们在==执行启动代码==的时候,进入
timers
的时间延迟其实是==随机的==,并不是确定的,所以会出现两个函数执行顺序随机的情况。那我们再来看一段代码:
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
阶段执行回调,之后在下一个事件循环再在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大家呢,可以先看着代码,默默地在心底走一变代码,然后对比输出的结果,当然最后三位,我个人认为是有点问题的,毕竟在主模块运行,大家的答案,最后三位可能会有偏差;
更多node相关知识,请访问:nodejs 教程!
위 내용은 Node.js의 이벤트 루프를 자세히 설명하는 기사의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!