先记住这个答案
poll 一方面取得并处理就绪 I/O,另一方面在合适条件下等待新事件。已有相关回调时会继续处理,但受内部限制等因素影响;队列为空时,是否等待及等待多久取决于后续工作、定时器期限和活动资源。存在已安排的 setImmediate 时,循环需要继续到 check,而不是无期限等新 I/O。没有保持循环存活的工作时还可能退出。poll 的系统等待通常不忙占 CPU,长 JavaScript 回调则会阻止事件循环及时处理其他任务,二者不能混为一谈。
- poll 既处理就绪事件,也负责适当等待
- 等待时间受定时器和其他就绪工作约束
- 高效等待不同于执行长同步 JavaScript
队列非空时先处理已有进展
例如网络数据已经就绪,poll 可以把相应回调交给 JavaScript。回调内部仍同步运行到交回控制,若每次处理过大数据或执行昂贵解析,其他任务就会受到影响,不会因为来源是异步 I/O 而自动可抢占。
运行时也有避免某一轮无限停留的内部机制,但它不能拯救业务回调内部的无限循环。应限制单次回调工作量,并把数据处理与就绪通知的职责分开,不能将公平性全部寄托给阶段切换。
队列为空不意味着必然一直睡眠
如果已经安排 setImmediate,循环有后续 check 工作需要处理;如果存在将到期定时器,等待也需要考虑其期限。I/O 新事件到来还可以使等待结束,因此空队列只是判断条件之一。
若没有任何需要维持运行的活动资源,事件循环可能结束,进程也可能退出。unref 等引用设置会影响这类存活条件,所以不能说只要调用过异步 API,poll 就会永久等待未来某个回调。
用 CPU 与延迟证据区分等待和阻塞
服务空闲时在系统等待中停留、CPU 使用低,可能是正常状态;某个同步函数持续运行导致其他回调不能执行,则需要查看 CPU 栈和事件循环延迟。二者都可以被口语称作等待,但性能含义完全不同。
下游网络很慢也会让请求变慢,却未必造成高事件循环延迟。诊断应同时观察请求阶段耗时、循环延迟和资源使用,避免看见响应慢就认定 poll 阻塞,或者看见空闲等待就盲目调整定时器。
容易答错的地方
- 把 poll 的系统等待当作需要消灭的性能问题
- 没有工作时高效等待可以节省 CPU,新 I/O 到来仍能唤醒处理。优化应针对不合理的业务等待或同步占用,而不是让空闲进程不停轮询,否则可能只增加资源消耗。
- 认为有定时器就完全不能等待 I/O
- 等待时间可以受定时器期限约束,并不必然变成一直空转。需要区分定时器已就绪和未来才到期,具体实现还受运行版本影响,不能把存在定时器当作所有 poll 行为的单一开关。
面试官还会怎么问?
setImmediate 为什么能帮助长计算让出机会?
把下一段计算安排到后续循环机会,可以让当前回调结束,并给 I/O 等工作获得处理机会。它不会把计算移到别的线程,单个计算片段仍需足够短,否则进入回调后仍然会阻塞。
poll 队列中的回调可以在中途被定时器打断吗?
普通 JavaScript 回调不会因为另一个定时器到期就自动被抢占。当前执行必须交回控制,之后运行时才有机会处理后续任务,因此缩短单次同步工作量仍然是响应性的关键。
怎样判断慢请求主要是在等下游服务?
记录请求中各阶段的时间,并结合事件循环延迟、CPU 栈和下游指标。如果等待期间循环仍能及时处理其他回调,更可能是外部依赖或排队;单一延迟数字不足以定位具体根因。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。