Node.js 学习笔记 —— 事件机制

# Node.js 学习笔记 —— 事件机制

# event loop

  • 每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈。Node.js的单线程指的是主线程是“单线程”。
  • Node.js 在主线程里维护了一个"事件队列"(Event queue),当用户的【网络请求或者其它的异步操作】到来时,Node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。
  • 主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,线程归还给线程池,等待事件循环。当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop)。
  • 主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完。

# event loop 执行顺序

  • 一共分为以下几个阶段,每个阶段都有一个FIFO(先进先出)队列来执行回调。当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行,然后事件循环移动到下一阶段。
  • timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
  • I/O callbacks 阶段:执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare 阶段:仅系统内部使用
  • poll 阶段:轮询。获取新的I/O事件, 执行I/O相关的回调。适当的条件下node将阻塞在这里
  • check 阶段:执行setImmediate()的回调
  • close callbacks 阶段:一些关闭的回调函数,比如socket的close

# 轮询

  • 两个重要功能:计算阻塞和轮询I/O的事件;处理轮询队列的事件。

# setImmediate 和 setTimeout

  • setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本。
  • setTimeout() 在最小阈值(ms 单位)过后运行脚本。
  • 在一个I/O循环内,调用的时候,setImmediate 总是优先于 setTimeout 调用。比如在一个读取文件的回调中执行。
const fs = require('fs');
fs.readFile('/path/file', function() {
	setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
})
  • 在非一个I/O循环内的脚步,比如主模块内执行,则调用的顺序是非确定的,受进程性能约束。

# process.nextTick

  • 当前操作完成后处理 nextTickQueue, 而不管事件循环的当前阶段如何。
  • 任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前解析。
  • 允许进行递归调用 process.nextTick(),但不能超过 V8 的最大调用堆栈大小 限制。
let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

function someAsyncApiCall1(callback) { callback(); }

someAsyncApiCall(() => {
  console.log("bar", bar); // 在 process.nextTick 的回调中,bar 为 1,异步执行,但在事件循环之前。
});

someAsyncApiCall1(() => {
  console.log("bar", bar); // bar 为 undefined,实际上为同步执行
});

bar = 1;

  • 对比 setImmediate
    • process.nextTick() 在同一个阶段立即执行。
    • setImmediate() 在事件循环的接下来的迭代或 'tick' 上触发。
  • 使用场景
    • 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
    • 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。

# 参考