前言
本文主要讨论:
当 Node.js 在处理会导致阻塞的任务时(如超大循环),如何让它在处理过程中将控制权交还给事件循环,以便处理其他任务,避免阻塞。
正文
核心问题:什么是"将控制权交还给事件循环"?
Node.js 的 JavaScript 代码是单线程运行的,依靠"事件循环"机制从多个队列中取出任务执行。
这些任务包括处理网络请求、文件读写回调、定时器回调等。
当一个函数(如很长的 for 循环)执行时,它会独占 线程。在它执行完毕前,事件循环被阻塞,无法处理其他等待中的任务(如新用户请求、数据库查询结果等),导致应用"卡住"。
"将控制权交还给事件循环" 就是主动中断当前的长任务,让事件循环有机会处理积压的其他任务,然后再继续执行原任务。
如何实现?
直接上代码:
javascript
// 1. 模拟繁重计算任务
async function heavyCalculation() {
for (let i = 0; i < 10000000; i++) {
console.log(i)
// 2. 关键:每循环1000次执行一次"让权"操作
if (i % 1000 === 0) {
// 3. 创建 Promise,使用 setImmediate 安排 resolve 在事件循环末尾执行
await new Promise(resolve => setImmediate(resolve))
// 4. 执行到 await 时函数暂停,主线程控制权释放
// 5. 事件循环可处理其他任务
// 6. 事件循环处理完后,执行 setImmediate 的 resolve
// 7. Promise 完成,函数恢复执行,继续下一个1000次循环
}
}
}
🤔 思考:setImmediate 是什么?
setImmediate是将回调函数放入事件循环宏任务队列的方法,在当前事件循环末尾、下一个事件循环开始时执行回调。
一图胜千言!
scss
执行 heavyCalculation() 循环 0-999
↓
遇到 await setImmediate(resolve) → 暂停函数!
↓
★★★ 控制权交还给事件循环 ★★★
↓
【轮询阶段】← 处理积压的 HTTP 请求! ✓
↓
【check阶段】← 执行 setImmediate(resolve)
↓
恢复 heavyCalculation() 循环 1000-1999
详细执行顺序:
- 注册任务 :
setImmediate(resolve)将 resolve 放入 check 阶段的宏任务队列 - 暂停函数 :
await让函数暂停,调用栈清空 - 事件循环继续:调用栈为空【✍️ 调用栈为空就是指当前没有正在执行的同步代码,JavaScript 引擎可以暂停下来去处理事件循环中的其他任务了。】,事件循环前进到轮询阶段
- 处理请求:在轮询阶段处理所有积压的用户请求
- 执行回调 :进入 check 阶段执行
resolve() - 恢复执行 :
await等待结束,函数继续执行
🤔 思考:为什么这样做不会阻塞且能中途处理 HTTP 请求?
因为 await setImmediate() 让当前函数"暂停"并清空调用栈,给了事件循环执行轮询阶段的机会,而 HTTP 请求正是在轮询阶段处理的!
🤔 思考:轮询阶段处理的是新请求还是积压请求?两者都会处理!既处理之前积压的请求,也处理在此期间新到达的请求。控制权交还后,事件循环会清空当前排队中的所有请求。
实际应用场景
-
处理大数据保持响应性
- 场景:遍历十万条记录进行复杂计算
- 问题:同步执行会导致服务器几秒内无响应
- 解决 :每处理 100 条记录就用
await setImmediate让出控制权
-
CPU 密集型任务分片
- 场景:长时间数学计算或图像处理
- 问题:长时间占用主线程阻塞一切
- 解决 :将大任务分割成小任务块,每块完成后用
setImmediate让事件循环处理其他任务
最后
"手动将控制权交还给事件循环"是一种编程技巧,通过主动地去中断长时间运行的同步代码,避免阻塞 Node.js 单线程的事件循环,保证应用保持响应能力,不至于"卡死"
本质上是将大块连续的计算任务分解成多个小任务,利用事件循环机制在任务间隙插入其他任务的处理机会。
🤔 小贴士:这种方法会影响性能吗?
会有轻微性能开销,因为增加了额外的任务调度。但在需要保持响应性的场景下,这种开销是值得的 ------ 用户宁愿等待 10 个 100ms 的间隙任务,也不愿面对 1 个 1s 的完全卡顿。