面试题答案
一键面试Node.js事件循环中任务队列工作原理
- 事件循环机制基础:Node.js基于事件驱动、非阻塞I/O模型运行。事件循环不断检查调用栈是否为空,若为空则从任务队列中取出任务放入调用栈执行。
- 任务队列概念:任务队列是存储待执行任务的队列结构。任务在合适时机进入队列,等待事件循环处理。
不同类型任务进入队列时机
- 宏任务(macrotask)
- 常见宏任务类型:包括
setTimeout
、setInterval
、setImmediate
、I/O 操作、script
(整体代码块)等。 - 进入时机:
setTimeout
和setInterval
:当调用这两个函数时,它们的回调函数会在指定时间(setTimeout
延迟时间、setInterval
间隔时间)后被放入宏任务队列。但需注意,这里的时间是近似值,实际放入队列时间可能因事件循环繁忙程度而延迟。setImmediate
:在当前轮次事件循环的检查阶段(check
阶段)将回调加入宏任务队列。它通常在 I/O 操作完成后的下一个阶段执行,并且在setTimeout
和setInterval
回调之后(如果它们在同一轮事件循环中到期)。- I/O 操作:当 I/O 操作完成时,其回调函数会被放入宏任务队列。
script
:在 Node.js 启动后,整个 JavaScript 脚本作为第一个宏任务进入队列并开始执行。
- 常见宏任务类型:包括
- 微任务(microtask)
- 常见微任务类型:
Promise.then
、process.nextTick
(Node.js 特定)、MutationObserver
(浏览器环境,Node.js 无此 API 但原理类似)等。 - 进入时机:
Promise.then
:当Promise
状态改变(resolve
或reject
)时,then
回调函数会被放入微任务队列。如果Promise
已经是resolved
或rejected
状态,那么then
回调会在当前调用栈执行完毕后立即放入微任务队列。process.nextTick
:在当前操作完成后,无论当前调用栈是否结束,process.nextTick
的回调函数都会立即被放入微任务队列。这使得它在微任务中的优先级相对较高。
- 常见微任务类型:
不同类型任务执行顺序
- 事件循环阶段与执行顺序:Node.js 事件循环有多个阶段,如
timers
(处理setTimeout
和setInterval
到期任务)、I/O callbacks
(处理大部分 I/O 回调)、idle, prepare
(内部使用)、poll
(获取新的 I/O 事件,执行 I/O 回调)、check
(执行setImmediate
回调)、close callbacks
(处理关闭回调,如socket.on('close', ...)
)。 - 宏任务执行:事件循环进入某一阶段时,会执行该阶段对应的宏任务队列中的任务,直到队列为空。例如在
timers
阶段,会执行到期的setTimeout
和setInterval
回调。 - 微任务执行:在每个宏任务执行完毕后,事件循环会立即执行微任务队列中的所有任务,直到微任务队列为空,然后再进入下一个事件循环阶段处理宏任务。如果在执行微任务过程中又产生了新的微任务,新微任务也会被加入队列并继续执行,直到微任务队列为空。
对应用性能的影响
- 宏任务影响:过多的宏任务,特别是像
setTimeout
和I/O
操作这类可能会阻塞事件循环的任务,如果处理不当,会导致事件循环长时间被占用,使得其他任务无法及时执行,造成应用响应延迟。例如,频繁执行大计算量的setTimeout
回调可能导致 UI 卡顿(在 Node.js 服务端虽无 UI,但类似客户端场景,会影响其他请求处理)。 - 微任务影响:虽然微任务执行在宏任务之后且不会阻塞事件循环,但如果在微任务中执行大量计算或产生过多微任务,可能会导致微任务队列过长,使得后续宏任务长时间得不到执行,同样影响应用性能。例如,在
Promise.then
回调中进行复杂的同步计算,会延迟下一个宏任务的执行。合理使用微任务可以优化异步操作,比如在 DOM 操作后通过Promise.then
进行后续处理,避免阻塞渲染线程(在浏览器环境),但滥用会适得其反。