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' 上触发。
- 使用场景
- 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
- 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。