《Node.js 事件循环》
Node.js 事件循环(Event Loop)详解
Node.js 的**事件循环(Event Loop)**是 Node.js 最核心的机制之一,也是面试中非常高频的问题。
理解事件循环,需要先理解一句话:
Node.js 使用单线程执行 JavaScript,通过事件循环和异步 IO,让一个线程能够处理大量并发请求。
1. 为什么 Node.js 需要事件循环?
JavaScript 本身是:
单线程语言意味着:
同一时间只能执行一段代码例如:
console.log("开始")
const result = fs.readFileSync("test.txt")
console.log(result)
console.log("结束")执行过程:
开始
↓
等待文件读取
↓
读取完成
↓
结束如果文件读取需要 5 秒:
整个程序阻塞 5 秒。
但是服务器中:
用户A 请求接口
用户B 请求接口
用户C 请求接口不能让一个用户的 IO 操作阻塞其他用户。
所以 Node.js 引入:
事件循环 Event Loop2. Node.js 的整体运行结构
Node.js 主要由几个部分组成:
JavaScript代码
↓
V8 JavaScript引擎
↓
Node.js运行环境
┌───────────┴───────────┐
Event Loop libuv
↓
操作系统线程池
↓
文件/网络/数据库其中:
V8
负责:
执行 JavaScript代码libuv
负责:
异步任务事件循环线程池网络IO3. 事件循环核心流程
Node.js 执行代码时:
同步代码
↓
微任务队列
↓
事件循环阶段
↓
宏任务
↓
重复循环简单理解:
先执行当前代码
再处理已经完成的异步任务
继续执行新的异步任务4. Event Loop 六个阶段
Node.js 的事件循环主要分为:
┌────────────────────┐│ timers │├────────────────────┤│ pending callbacks │├────────────────────┤│ idle prepare │├────────────────────┤│ poll │├────────────────────┤│ check │├────────────────────┤│ close callbacks │└────────────────────┘每一轮循环称为:
Tick5. 六个阶段详细解释
第一阶段:timers
负责执行:
setTimeout()
setInterval()例如:
setTimeout(()=>{
console.log("timer")
},1000)1 秒后进入 timers 阶段执行。
注意:
setTimeout(1000) 不是保证 1000ms 后执行,而是至少等待 1000ms。
例如:
如果主线程正在执行:
while(true){}那么 timer 永远不会执行。
第二阶段:pending callbacks
处理一些系统级回调。
例如:
- TCP错误
- 网络异常
普通开发中接触较少。
第三阶段:idle、prepare
Node.js 内部使用。
开发者基本不会接触。
第四阶段:poll(最重要)
poll 是事件循环的核心阶段。
主要处理:
- IO 回调
- 文件读取
- 网络请求
例如:
const fs = require("fs")
fs.readFile("./a.txt",()=>{
console.log("读取完成")
})流程:
调用readFile
↓
交给系统处理
↓
Node继续执行其他代码
↓
文件读取完成
↓
poll阶段执行回调第五阶段:check
执行:
setImmediate()例如:
setImmediate(()=>{
console.log("immediate")
})会在 check 阶段执行。
第六阶段:close callbacks
执行关闭事件。
例如:
socket.on("close",()=>{
})6. 宏任务和微任务
这是面试重点。
Node.js 中任务分两类:
宏任务(Macrotask)
包括:
setTimeout
setInterval
setImmediate
IO回调微任务(Microtask)
包括:
Promise.then
catch
finally
process.nextTick执行优先级:
同步代码
↓
process.nextTick
↓
Promise.then
↓
timer
↓
IO
↓
setImmediate7. 一个经典面试题
代码:
console.log("start")
setTimeout(()=>{
console.log("timeout")
},0)
Promise.resolve().then(()=>{
console.log("promise")
})
console.log("end")输出:
startendpromisetimeout为什么?
第一步:执行同步代码
console.log("start")输出:
start遇到:
setTimeout()放入 timer 队列。
遇到:
Promise.then()放入微任务队列。
继续:
console.log("end")输出:
end第二步:执行微任务
Promise 优先:
输出:
promise第三步:进入事件循环
执行 timer:
输出:
timeout最终:
startendpromisetimeout8. process.nextTick 和 Promise 的区别
代码:
process.nextTick(()=>{
console.log("nextTick")
})
Promise.resolve().then(()=>{
console.log("promise")
})结果:
nextTick
promise原因:
Node.js 对:
process.nextTick优先级最高。
执行顺序:
同步代码
↓
nextTick
↓
Promise
↓
Event Loop9. setTimeout 和 setImmediate 区别
经典问题:
setTimeout(()=>{
console.log("timeout")
},0)
setImmediate(()=>{
console.log("immediate")
})输出?
答案:
不确定。
原因:
两个属于不同阶段:
setTimeout
↓
timers阶段
setImmediate
↓
check阶段第一次循环时:
取决于环境。
但是:
如果在 IO 回调里面:
fs.readFile("test.txt",()=>{
setTimeout(()=>{
},0)
setImmediate(()=>{
})
})通常:
输出:
immediate
timeout因为:
IO回调结束后:
poll
↓
check
↓
timers10. async/await 与事件循环
例如:
async function test(){
console.log(1)
await Promise.resolve()
console.log(2)
}
test()
console.log(3)执行:
输出:
132原因:
await 后面的代码:
console.log(2)会进入:
Promise微任务队列11. Node.js 为什么单线程还能高并发?
关键:
JavaScript线程
负责:
执行代码处理事件循环IO任务
交给:
操作系统
或者
libuv线程池例如:
文件读取:
JavaScript线程
↓
libuv线程池
↓
读取文件
↓
通知Event Loop
↓
执行回调所以:
Node.js:
一个JavaScript线程
+
多个异步IO任务
=
高并发12. 面试回答版本(重点)
如果面试官问:
Node.js 事件循环是什么?
可以回答:
Node.js 事件循环是一种异步非阻塞机制,它允许 Node.js 在单线程中处理大量并发任务。JavaScript 主线程执行同步代码,当遇到 IO、定时器等异步任务时,会交给底层 libuv 处理,任务完成后将回调放入对应队列,事件循环按照不同阶段依次执行这些回调。其中包括 timers、poll、check 等阶段,同时微任务(process.nextTick、Promise)会优先于宏任务执行。
一句话总结:
Node.js 事件循环就是:通过一个主线程不断检查任务队列,把已经完成的异步任务回调取出来执行,从而实现单线程下的高并发处理。 转换为 markdown 格式
《Node.js 事件循环》
作者:felinus
本文链接: https://felinus-blog.vercel.app/posts/f8882815/
本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。