JS 通过事件循环来处理并发。
将来使用新执行栈调用的函数称为异步运行;反之,在同一执行栈执行的函数称为同步运行。
Node.js 的层:
- 应用程序代码 - JS
- Node.js 核心 API - JS
- Node.js binding - C++
- V8(JS 引擎)、libuv、OpenSSL... - C++
- 操作系统
I/O 操作通常需要花费较长时间。Node 本身是多线程的,其底层使用的 libuv 可以处理操作系统抽象及管理 I/O 操作,并维护一个线程池。当 JS 正常运行时,Node.js 内部可能正忙于读取文件内容。
当没有异步任务来让进程保持活动状态时,Node.js 就会终止退出。Node 也提供了一些方式使异步任务不再保持进程为活动状态,比如:
const timer = setTimeout(() => {}, 1000);
timer.unref(); // 仍然会执行回调,但不会让进程保持活动状态。
循环队列有以下几个阶段:
- 轮询 - 执行 I/O 相关的回调。主代码开始时会在这个阶段运行。
- 检查 - 执行
setImmerdiate()
触发的回调。 - 关闭 - 执行通过
EventEmitter
的close
事件触发的回调。 - 定时器 - 执行
setTimeout()
或setInterval()
触发的回调。 - 挂起 - 运行特殊的系统事件。
每个阶段都会维护一个待执行的回调队列,在 JS 代码运行时,就会往不同队列里添加回调。
还有两个微任务队列,会在每个阶段工作前执行:
process.nextTick()
Promise
其中 process.nextTick()
会优于 Promise
执行。
事件循环注意点
在单个执行栈里不要运行太多代码,这样会导致事件循环暂停并阻止触发其他回调。可以将大型任务分批处理,在每个批次结束时通过 setImmediate()
来执行下一个批次。
不要用 process.nextTick()
做类似下面代码的事情,会导致微任务队列永远不会清空,应用程序会永远困在同一阶段里:
const forever = () => process.nextTick(forever);
forever();
setInterval(() => console.log('hello'), 10); // 永远不会执行
在设计一个函数或者 API 时,最好保证其要么是同步或者异步执行,比如:
function foo(count, callback) {
if (count <= 0) {
// 该回调是同步调用的
return callback(new TypeError('count > 0'));
}
myAsyncOperation(count, callback);
}
为了确保回调是在一个新栈中执行,可以用 process.nextTick()
或者 setImmediate()
包裹同步代码。