【Node.js】事件循环

Node.js 中的事件循环是基于单线程的异步非阻塞模型。它是 Node.js 的核心机制,用于处理非阻塞的 I/O 操作和异步事件。
在这里插入图片描述

1. Node.js 事件循环介绍

Node.js 的事件循环是一个 Event Loop,通过异步回调函数的方式实现非阻塞的处理。事件循环会在主线程上不断地执行,监听和处理事件,执行相应的回调函数。

Node.JS的事件循环比浏览器端复杂很多。Node.js的运行机制如下:

  • V8引擎解析JavaScript脚本。
  • 解析后的代码,调用Node API 。
  • libuv 库(C++)负责Node API的执行。它将不同的任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给V8引擎。
  • V8引擎再将结果返回给用户。
    在这里插入图片描述

2. 事件循环的六个阶段

libuv 引擎中的事件循环分为六个阶段,每个阶段都有对应的回调队列(回调函数的集合)和触发器,依次执行以下步骤:

  • timers 阶段:处理定时器和 setTimeout/setInterval 设置的回调函数。
  • I/O callbacks 阶段:处理上一轮循环中少量未执行的与 I/O 相关的回调函数,例如网络请求的响应、文件读写等。
  • idle, prepare 阶段:内部使用。
  • poll 阶段:检索新的 I/O 事件,执行与 I/O 相关的回调函数。
  • check 阶段:执行 setImmediate 设置的回调函数。
  • close callbacks 阶段:执行一些关闭的回调函数,例如 socket.on('close', ...)
    在这里插入图片描述

外部输入数据一>轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器
检测阶段(timer)->I/O事件回调阶段(I/O callbacks)->闲置阶段(idle、prepare)->轮询阶段(按照该顺序反复运行)

timers

timers阶段会执行setTimeout和setInterval回调,并且是由poll阶段控制的。同样,在Node.js中定时器指定的时间也不是准确时间,只能是尽快执行。

poll

poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情:

  • 回到timer阶段执行回调
  • 执行 I / O 回调
    并且在进入该阶段时如果没有设定了timer的话,会发生以下两件事情:
  • 如果 poll 队列不为空,会遍历 poll 回调队列并同步执行,直到队列为空或者达到系统限制
  • 如果 poll 队列为空时,会有两件事发生:
    • 如果有setlmmediate回调需要执行,poll 阶段会停止并且进入到check阶段执行回调
    • 如果没有setlmmediate回调需要执行,会等待其他异步任务回调被加入到队列中并立即执行回调,这里同样会有个超时时间,防止一直等待下去

当然设定了timer的话且 poll 队列为空,则会判断是否有timer超时,如果有的话会回到timer阶段执行回调。

假设 poll 被堵塞,那么即使 timer 已经到时间了也只能等着,这也是为什么上面说定时器指定的时间并不是准确的时间。

const fs = require('fs')
const start = Date.now()
setTimeout(()=>{
    console.log('setTimeout', Date.now() - start)  // 503ms
}, 200)
fs.readFile('./index.js', ()=>{
    const start = Date.now()
    console.log('文件读取结束')
    // 强行拖时间
    while(Date.now() - start < 500) {}
})
//timer队列 setTimeout 异步需要等待 poll 全部执行完之后再执行
//poll队列 readFile

check

setimmediate()的回调被加入check队列中,从事件循环的阶段图可以知道,check阶段的执行顺序在poll 阶段之后。

3. 一些注意点:

  • setTimeout 和 setImmediate 区别:setImmediate 在 poll阶段完成时执行,即check阶段;setTimeout 在 poll 阶段为空闲时,且设定时间到达后执行,但在 timer 阶段执行
setTimeout(function() {
    console.log('timeout');
}, 0)
setImmediate(function() {
    console.log('immediate');
})

他们执行的先后顺序是未知的,进入事件循环的准备也是需要花费成本的,如果准备阶段花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调;否则就先执行 setImmediate 回调。

但当二者在异步 I/O callback内部调用时,总是先执行setlmmediate, 再执行setTimeout。例如:

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

在上述代码中,setlmmediate永远先执行。因为两个代码写在I/O回调中,I / O 回调是在 poll 阶段执行,当回调执行完毕后队列为空,发现存在setlmmediate回调,所以就直接跳转到check阶段去执行回调了。

  • process.nextTick
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')
            })
        })
    })
})
// timers: setTimeout
// 微任务队列:nextTick

nextTick 有一个自己的队列,独立于事件循环,每个阶段执行完成后,如果存在 nextTick,就会清空队列中的所有回调函数,并且优先于其他 微任务队列 执行。

4. Node.js 事件循环和浏览器事件循环的区别:

  • 浏览器环境下,微任务队列在每一个宏任务执行完之后执行。
  • node.js 环境,微任务会在事件循环的各个阶段之间执行,每一个阶段执行完毕,都会去清空微任务队列中的任务。

通过事件循环,Node.js 实现了高效的异步 I/O 操作,并能够处理大量的并发连接,适用于构建高性能的网络应用程序。

  • 48
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小秀_heo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值