node入门小白,下面是自己的总结,如哪里有不对的地方,感谢指出来。
一、重点
1.事件循环
2.事件驱动程序
3.EventEmitter
二、内容
1.node事件循环(看之前需先自行了解下 线程、进程)
这里先说下node.js运行机制,node.js采用v8作为js的解析引擎,而在I/O处理方面采用了libuv。libuv库负责node api的执行,它将不同的任务分给不同的线程,形成一个Event Loop。以异步的方式将执行的结果返回给V8引擎。下面直接上图:
了解了node运行机制后,咱们再来看libuv中的事件循环,其中 libuv 引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段(为了避免函数在poll阶段阻塞)。下面看下对应的六个阶段的图:
我理解的事件循环规则:
1.每个阶段都有会执行各自对应的回调函数
2.每个阶段在执行完各自的回到函数后,都要去检查该阶段是否有process.nextTick任务和microtask(微任务)如果有的话并去执行。
下面我们详细说下每个阶段的原理和顺序:
第一步node初始化
- 初始化 node 环境。
- 执行输入代码。
- 执行 process.nextTick 回调。(后续每个阶段都会执行该操作,比作a)
- 执行 microtasks。(后续每个阶段都会执行该操作,比作b)
第二步进入event-loop(事件循环)
(1)进入 poll 阶段
- 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。
- 第一种情况:
- 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
- 执行a,b,如果有,全部执行。
- 退出该阶段。
- 第二种情况:
- 如果没有可用回调,检查是否有 setImmediate 回调,如果有,退出 poll 阶段。如果没有,会先等待回调,如果超时会被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去。
- 第一种情况:
- 如果不存在尚未完成的回调,退出poll阶段。
(2) 进入check 阶段
- 如果有setImmediate()回调,则执行所有setImmediate回调。
- 执行a,b,如果有,全部执行。
- 退出该阶段。
这里说下两个注意点: (1) setTimeout 和 setImmediate 二者非常相似,区别主要在于调用时机不同。setTimeout可能执行在前也可能在后,为什么这么说,因为进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调。但当二者在异步 i/o callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout 结果如下:
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// immediate
// timeout
- setImmediate 设计在 poll 阶段完成时执行,即 check 阶段;
- setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行
(2) process.nextTick 这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
(3)进入 close callbacks阶段
- 当一个socket连接或者一个handle被突然关闭时(例如调用了
socket.destroy()
方法),close事件会被发送到这个阶段执行回调。 - 执行a,b,如果有,全部执行。
- 退出该阶段。
(4)进入 timers 阶段
- 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
- 执行a,b,如果有,全部执行。
- 退出该阶段。
**(5) **进入IOcallbacks阶段
- 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
- 执行a,b,如果有,全部执行。
- 退出该阶段。
(6) 进入idle,prepare 阶段(这两个阶段与我们编程关系不大,暂且不说)
以上是总结的事件循环的过程和原理,整个过程我们可以换个角度去理解:
固执的探险家(每个房间都要走到底)
所有的代码就像是已经设定好的迷宫,而引擎就是去探险的人,我们称为小呆。
小呆到达迷宫,已经有了地图,根据地图冒险。
迷宫有六个房间,分别是timer, i/ocallback, ide prepare(内部使用,已封闭), poll, check, close callback,
其中timer是虚拟现实房间,小呆随时可以看到里面的场景。
其他的每个房间又五个房间,有的开放,有的不开放。
探险规则:每次离开一个房间,都要检查有没有受伤(peocess.nextTick())和检查有没有丢下物品没有处理(microtasks微任务)
小呆首先进入poll房间,开始探险(执行poll 队列),之后进入check房间,timer房间(随机),探险完之后出来,进入close callback,探险完之后,进入io/callback房间,最后完成探险,离开。
小呆说任务总算完成了。
node中的事件循环和浏览器中的事件循环有所差异不要混为一谈,浏览器中的事件循环可以先参考这里看下juejin.cn/post/684490…
本周继续更新2.事件驱动程序3.EventEmitter