Skip to content

Nodejs 事件循环

理解

非阻塞 I/O 操作的设计机制

  • Timer: 定时器运行 setTimeout setInterval
  • Pending Callback: 挂起的回调,执行延迟到下一个循环迭代的 I/O 回调(TCP 的错误处理延迟到这里执行)
  • Idle, Prepare: 仅在内部使用
  • Poll: 轮询,检索新的 I/O 事件,执行与 I/O 相关的回调(计时器和 setImmediated()调度之外)
    • 计算应该阻塞和轮询 I/O 的时间
    • 处理轮询队列中的事件(没有被调度的计时器时)
      • 轮询队列为空
        • 如果脚本被 setImmediate() 调度,则事件结束轮询阶段,并到 Check 阶段继续执行 setImmediate() 调度的脚本
        • 未被 setImmediate() 调度,则事件循环将等待回调被添加到队列中,然后立即执行
      • 轮询队列不为空
        • 事件循环将循环访问回调队列并执行他们,直到队列用尽,或者达到了与系统相关的硬性限制
  • Check: setImmeidate 检查是否执行
  • Close Callback: 关闭的回调函数(socket.on('close))

process.nextTick 又是如何执行?

  • 异步 API

任何时候再给定的阶段中调用 process.nextTick(),所以传递到 process.nextTick() 的回调将在事件循环继续之前解析。

  • 造成任务饿死

一直调用 process.nextTick() 从而导致事件循环无法到达轮询阶段(递归调用 process.nextTick() 不允许超过 V8 的最大调用堆栈大小)

事件循环图

┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ idle, prepare │ │ └─────────────┬─────────────┘ ┌───────────────┐ │ ┌─────────────┴─────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └─────────────┬─────────────┘ │ data, etc. │ │ ┌─────────────┴─────────────┐ └───────────────┘ │ │ check │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ └──┤ close callbacks │ └───────────────────────────┘

定时器 timers

本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数

  • 执行所提供的回调的阈值,而不是用户希望其执行的确切时间(计时器回调尽可能早的运行,但是操作系统调度或其它正在运行的回调可能会存在延迟)
js
// 100ms !== 阈值
setTimeout(function() {}, 100);
// 100ms !== 阈值
setTimeout(function() {}, 100);
  • 轮询阶段控制何时定时器执行

待定的回调 pending callbacks

执行延迟到下一个循环迭代的 I/O 回调

  • 挂起的回调函数(持续连接 TCP 套接字在尝试连接时接收到 ECONNREFUSED)

idle prepare

系统内部使用

轮询 poll

检索新的 I/O 事件,执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,计时器和 setImmediate() 调度的之外)其余情况 node 将在适当的时候阻塞

  • 计算应该阻塞和轮询 I/O 的时间

  • 处理轮询队列里的事件

    • 轮询队列不为空,事件循环将循环访问回调队列并同步执行他们,直到队列为空,或者达到了与系统相关的硬性限制
    • 轮询队列为空,如果脚本被 setImmediate() 调度,则事件循环将结束轮询阶段,并继续检查阶段以执行哪些被调度的脚本;如果脚本未被 setImmediate() 调度,则事件循环将等待回调被添加到队列中,然后立即执行
  • 一旦轮询队列为空,事件循环将检查已达到阈值的计时器,如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行计时器回调

检测 check

setImmediate() 回调函数在这里执行

  • 轮询阶段完成后立即执行回调,轮询阶段变为空闲状态,脚本使用 setImmediate() 后被排列在队列中,则事件循环可能继续到检查阶段而不是等待
  • setImmediate() 事件循环中的单独阶段运行的特殊及时器(libuv API 安排回调在轮询阶段完成后立即执行)

关闭的回调函数 close callbacks

一些关闭的回调函数 socket.o('close', ...)

  • 套接字或处理函数关闭(socket.destroy()),close 事件在这个阶段发出,否则(process.nextTick()) 发出

setTimeout() VS setImmediate()

  • setImmediate() 一旦在当前轮询阶段完成,就执行脚本
  • setTimeout() 轮询阶段计算出来的阈值(ms)过后运行脚本
  1. 主模块中运行(非确定性)
  2. 运行在 I/O 循环内调用,setImmediate 总是被优先调用

process.nextTick

  • 不是事件循环的一部分,在当前操作完成后处理 nextTickQueue,允许通过递归调用来阻止I/O,阻止事件到达轮询阶段

  • 保证用户代码的其余部分之后在让事件循环继续进行之前,执行其回调函数

  • 回调置于 process.nextTick() 中,脚本仍具有运行完成的能力,允许在调用回调之前初始化所有的变量、函数

  • 不让事件循环继续的优点,适用于让事件循环继续之前,警告用户发生错误的情况

process.nextTick() VS setImmediate()

  • process.nextTick() 在同一个阶段立即执行
  • setImmeidate() 在事件循环的接下来的 tick 上触发

为什么使用 process.nextTick()

  1. 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求
  2. 让回调在栈展开后,但在事件循环继续之前运行的必要

Released under the MIT License.