面试题答案
一键面试事件循环机制概述
Node.js 采用事件驱动、非阻塞 I/O 模型,事件循环是实现这一模型的核心机制。它持续检查事件队列,依次处理队列中的事件。
事件循环的各个阶段及其作用
- timers(定时器阶段)
- 作用:这个阶段执行已经安排好的
setTimeout()
和setInterval()
回调函数。它会检查定时器队列中到期的定时器,将其回调函数加入到这个阶段的执行队列中执行。
- 作用:这个阶段执行已经安排好的
- pending callbacks(待定回调阶段)
- 作用:执行系统底层的一些回调,例如 TCP 连接错误等底层操作的回调。这些回调是由操作系统或 Node.js 内部模块在合适的时候放入队列中的。
- idle, prepare(闲置、准备阶段)
- 作用:仅在内部使用,Node.js 内部在此阶段做一些准备工作,一般开发者很少直接与之交互。
- poll(轮询阶段)
- 作用:这是事件循环中非常重要的一个阶段。在这个阶段,事件循环会检查是否有新的 I/O 事件,如果有则执行它们的回调函数。同时,如果没有定时器到期,事件循环会在这里等待新的 I/O 事件。如果有已到期的定时器,事件循环会结束轮询阶段,进入到
timers
阶段执行定时器回调。
- 作用:这是事件循环中非常重要的一个阶段。在这个阶段,事件循环会检查是否有新的 I/O 事件,如果有则执行它们的回调函数。同时,如果没有定时器到期,事件循环会在这里等待新的 I/O 事件。如果有已到期的定时器,事件循环会结束轮询阶段,进入到
- check(检查阶段)
- 作用:此阶段专门执行
setImmediate()
安排的回调函数。setImmediate()
的回调会被加入到这个阶段的队列中执行,它与setTimeout()
不同,setImmediate()
总是在poll
阶段之后,timers
阶段之前执行(如果当前事件循环中poll
阶段没有 I/O 事件需要处理且没有定时器到期)。
- 作用:此阶段专门执行
- close callbacks(关闭回调阶段)
- 作用:执行一些关闭相关的回调,例如
socket.on('close', ...)
的回调函数,当一个 socket 或 handle 被关闭时,相应的回调会在这个阶段执行。
- 作用:执行一些关闭相关的回调,例如
长时间运行的同步任务的影响
如果在事件循环中出现一个长时间运行的同步任务,会阻塞事件循环。因为事件循环是单线程的,在同步任务执行期间,其他任务(包括非阻塞 I/O 操作的回调)都无法执行。这意味着新的 I/O 事件即使发生了,其回调也会被阻塞在队列中,无法及时得到处理,导致应用程序看起来像是“卡死”,响应变得迟缓。
解决方法
- 使用异步函数和回调:将同步任务改写成异步任务,例如使用
fs.readFile()
代替fs.readFileSync()
来读取文件,这样 I/O 操作就不会阻塞事件循环。 - Web Workers(浏览器环境类似概念):虽然 Node.js 没有完全等同于浏览器 Web Workers 的概念,但可以使用
child_process
模块创建子进程来处理计算密集型任务。子进程在独立的线程或进程中运行,不会阻塞主线程的事件循环。 - 使用队列和任务调度:可以创建一个任务队列,将长时间运行的任务分割成小块,每次从队列中取出一块任务执行一小段时间,然后让出事件循环,让其他任务有机会执行。例如,可以使用
setImmediate()
或process.nextTick()
来调度这些小块任务的执行。