node事件机制详解

事件轮询机制

事件执行顺序:

引入一下两个概念:

  1. 宏任务(Macrotasks):就是参与了浏览器事件循环的异步任务
    宏任务有:setTimeout,setInterval
  2. 微任务(Microtasks): 直接在 Javascript 引擎中的执行的,没有参与浏览器事件循环的任务。
    微任务:promise的then,await后面的语句,process.nextTick

执行的顺序是**‘先同后异,先微后宏’**

事件轮询机制阶段:

  • 定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
  • 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • 轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • 检测:setImmediate() 回调函数在这里执行。
  • 关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’, …)。

定时器:本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。

会在轮询阶段最后执行

待定回调:执行延迟到下一个循环迭代的 I/O 回调。

系统操作的回调

idle, prepare:仅系统内部使用。

轮询:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。

  1. 队列不为空:循环访问回调队列,并同步执行
  2. 队列为空:
  3. 有setImmidiate(cb),结束轮询,到达检查阶段执行setImmidiate的cb
  4. 无setImmidiate(cb),被加入队列,并同步执行事件。
无setImmidiate(cb)举例:

一般情况下是这样

一个的时候:

   new Promise((resolve, reject) => { console.log(1); resolve(2)})
    .then(data => console.log(data)) 

第一次进入轮询阶段,轮询队列为空 && 无setImmidiate,则then会被加入到队列,console.log(1); resolve(1) 会被执行。then需要等待下次轮询。
到第二次进入轮询阶段,则队列里面有then,则调用then的回调data=>console.log(data)

多个的时候:

new Promise((resolve, reject) => {
  console.log(1)
  resolve(2)
}).then(console.log)

new Promise((resolve, reject) => {
  console.log(5)
  resolve(6)
}).then(data => {
  console.log(data)
  new Promise((resolve, reject) => {
    console.log(7)
    resolve(8)
  }).then(console.log)
  new Promise((resolve, reject) => {
    console.log(9)
    resolve(10)
  }).then(console.log)
})

new Promise((resolve, reject) => {
  console.log(3)
  resolve(4)
}).then(console.log)

console.log(0)

以上很可看出执行顺序是:

第一次轮询:
  同步:1530
  进入微任务:2, 6, 7, 9, 4,

进入下一次轮询
  进入微任务:8,10
有setImmidiate(cb)举例:
setImmediate(console.log, 0)

new Promise((resolve, reject) => {
  console.log(11)
  resolve(12)
}).then(console.log)

function asyncFn(cb) {
  setImmediate(cb, 3) 
}
asyncFn(data => {
  new Promise((resolve, reject) => {
    console.log(data)
    resolve(6)
  }).then(console.log)
  new Promise((resolve, reject) => {
    console.log(7)
    resolve(8)
  }).then(console.log)
})

setImmediate(console.log, 4)

new Promise((resolve, reject) => {
  console.log(13)
  resolve(14)
}).then(console.log)

console.log(5)
同步:1113,5
异步:
  进入轮询阶段
    队列不为空,执行回调:12,14
    队列为空:执行setImmediate
结束轮询
检查阶段:0
进入轮询阶段
  队列为空
结束轮询
检查阶段:37
进入轮询阶段
  队列不为空:6,8
  轮询为空:执行setImmediate
结束轮询
检查阶段:4
    

这里跟promise的区别就是setImmediate会被轮询任务卡住,只有不为空的情况下才会被执行,其顺序是在每层轮询后才会被执行。

检测:setImmediate() 回调函数在这里执行。

执行回调

关闭的回调函数:一些关闭的回调函数,如:socket.on(‘close’, …)。

如果套接字或处理函数突然关闭(例如 socket.destroy()),则’close’ 事件将在这个阶段发出。否则它将通过 process.nextTick() 发出。

关注的问题:

process.nextTick() vs setImmediate() vs setTimeout()

从功能上来讲,其实setImmidiate的效果是下一次轮询前的执行,即nextTick。而process.nextTick的效果是在轮询阶段执行,刚好是Immediate的意思。这两者的名字应该是调转过来才对。

setTimeout: 最后执行

异步中任务顺序:

setImmediate(console.log, 0)

// 关注这两个
setTimeout(console.log, 0, 999) 
setTimeout(console.log, 10, 1000)

new Promise((resolve, reject) => {
  console.log(11)
  resolve(12)
}).then(console.log)

function asyncFn(cb) {
  setImmediate(cb, 3) 
}
asyncFn(data => {
  new Promise((resolve, reject) => {
    console.log(data)
    process.nextTick(console.log, 119)
    resolve(6)
  }).then(console.log)
  new Promise((resolve, reject) => {
    console.log(7)
    process.nextTick(console.log, 229)
    resolve(8)
  }).then(console.log)
})

setImmediate(console.log, 4)

new Promise((resolve, reject) => {
  console.log(13)
  resolve(14)
}).then(console.log)
/* 
  11
  13
  12
  14
  999
  0
  3
  7
  119
  229
  6
  8
  4
  1000
*/


setTimeout是看其阈值情况而定,

如果小于其阈值,比如为0,是在一次轮询后立即执行
promise.then = await的下一条语句 > process.nextTick > setTimeout > setImmediate

如果大于其阈值,比如设置300,则是多次轮询后,最后执行
promise.then = await的下一条语句 > process.nextTick > setImmediate > ...多轮 > setTimeout

以上就是为什么会在代码中经常看到使用setTimeout(fm, 0)或者setTimeout(fm, 300)的原因。

建议:

  1. 开发人员在所有情况下都使用 setImmediate(),因为它更容易理解

  2. 使用 setImmediate() 相对于setTimeout() 的主要优势是,如果setImmediate()是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关

我要注意什么事情?

如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束:
I/O 循环内调用,setImmediate 总是被优先调用:

比如:

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

为什么要使用 process.nextTick()?

有两个主要原因:

  • 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。

  • 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。

例子1:

const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
  EventEmitter.call(this);
  // 执行构造函数,出发event事件但监听的代码还没执行到,触发的事件并未被监听到,下面的代码不会被执行。
  // this.emit('event');
  // 捕获到错误,并加入事件队列放到下次轮询中调用
  process.nextTick(() => {
    this.emit('event');
  })
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter(); 
myEmitter.on('event', () => {
  console.log('an event occurred!');
});

例子2:

const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });

这个例子效果同上,是我们常用常见的写法。其实一般我们会把.on写在事件方式前,提前进行监听,这样才能捕捉到事件

参考:https://www.bookstack.cn/read/nodejs-guide/590a0c76ccf949ba.md#gxvmd


最后附上规律方便大家解题,一般面试题都会问到

事件循环机制规律:

  • 同步,promise内,await后的函数
  • 异步
    • 微任务 promise、await、process.nextTick、setImmediate
      优先级:promise.then = await的下一条语句 > process.nextTick > setImmediate > setTimeout
    • 宏任务 setTimeout setInterval
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值