小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
Node.js 简介
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。Node.js 的包管理器 npm, 是全球最大的开源库生态系统
Node.js是一个开源和跨平台的JavaScript运行时环境。
Node.js 的特点
- 异步的 I/O
- 事件机制
- 单线程
- 跨平台

Node.js 的运行机制
V8引擎解析javaScript脚本,解析后的代码,调用Node的APIlibuv库负责API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎V8引擎再将结果返回给用户
Node.js 事件循环机制
node 的 Event Loop 发生在 libuv 层,libuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API。

1. timers
执行 setTimeout() 和 setInterval()中的 callback,并且是由 poll 阶段控制的。同样,在 Node.js 中定时器制定的时间也是不准确时间,只能是尽快执行。
2. pending callback
执行上一次推迟到这一次循环迭代的 I/O 回调
3. idle, prepare
只在内部使用
4. poll
获取新的 I/O 事件,适当条件下 Node.js 将阻塞在这里
poll 是一个至关重要的阶段,这一阶段中,系统会做两件事情:
- 回到timer 阶段执行回调
- 执行 I/O 回调
并且在进入该阶段时如果没有设定了 timer 的话,会发生以下两件事情:
- 如果poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
- 如果poll 队列为空时,会有两件事发生:
- 如果有setImmediate 回调需要执行,poll 阶段会停止并且进入到check 阶段执行回调
- 如果没有setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
当然设定了timer 的话且 poll 队列为空,则会判断是否有timer 超时,如果有的话会回到timer 阶段执行回调。
假设 poll 被阻塞,那么即使 timer 已经到时间了也只能等着,这也是为什么上面说定时器指定的时间并不是准确的时间。例如:
const start = Date.now(); // 获取当前的时间戳
setTimeout(function f1() {
// f1 进入timer 队列
console.log("setTimeout", Date.now() - start)
}, 200)
const fs = require("fs")
fs.readFile('./index.js', 'utf-8', function f2() {
// f2 进入到 poll 任务队列
console.log('文件读取结束')
const start = Date.now()
// 强行延迟 500 毫秒
while(Date.now() - start < 500) { }
})
执行结果如下:
5. check
执行 setImmediate()的 callback
setImmediate() 的回调会被加入 check 队列中,从事件循环的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后。看个例子:
Promise 会被放入到微任务队列
会先清空微任务队列,再执行其他任务队列的回调任务**
console.log('start');
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise1');
})
}, 0)
setTimeout(() => {
console.log('timer2');
Promise.resolve().then(() => {
console.log('promise2');
})
}, 0)
Promise.resolve().then(() => {
console.log('promise3');
})
console.log('end')
// 输出结果:start end promise3 timer1 promise1 timer2 promise2
一开始执行同步任务,依次打印出 start end ,并将2个 timer 依次放入 timer 队列,之后会立即执行微任务队列,所以打印出 promise3。
然后进入 timer 阶段,执行 timer1 的回调函数,打印 timer1 ,发现一个 promise.then 回调将其加入到微任务队列并且立即执行,之后同样的步骤执行 timer2 , 打印 timer2 以及promise2。
需要注意
setTimeout 和 setImmediate 区别
二者非常相似,区别主要在于调用时机不同。
- setImmediate 设计在 poll 阶段完成时执行,即 check 阶段
- setTimeout 设计再 poll 阶段为空闲时,且设定时间(时间阈值)到达后执行,但它在 timer 阶段执行
看个例子:
setTimeout(function f3() {
console.log("setTimeout0")
}, 0)
setImmediate(function f4() {
console.log("setImmediate")
})
对于以上代码来说,setTimeout 可能执行在前,也可能执行在后。首先 setTimeout(fn, 0) === setTimeout(fn, 1),这是由源码决定的,进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调。如果在准备时候花费了小于 1ms ,那么就是 setImmediate 回调先执行了。
但当二者在异步I/O callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout,例如:
const fs = require("fs")
fs.readFile('./test.js', 'utf-8', function f5() {
console.log('文件读取结束5')
setTimeout(() => {
console.log('setTimeout5')
}, 0)
setImmediate(() => {
console.log('setImmediate5')
})
})
// 输出:setImmediate5 setTimeout5
在上述代码中, setImmediate 永远先执行。因为两个代码写在 I/O 回调中,I/O 回调在 poll 阶段执行,当I/O 回调执行完毕后队列为空,发现 setImmediate 回调,所以就直接跳转到 check 阶段去执行 setImmediate 回调了。
6. close callback
执行close事件的callback。e.g:socket.on('close', ...).
process.nextTick()
这个函数其实是独立于事件循环之外的,它有一个自己的队列。当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其它 microtask 执行。
setTimeout(() => {
console.log('setTimeout6')
Promise.resolve().then(() => {
console.log('Promise6')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick2')
process.nextTick(() => {
console.log('nextTick3')
process.nextTick(() => {
console.log('nextTick4')
})
})
})
})
// 输出:
nextTick
nextTick2
nextTick3
nextTick4
setTimeout6
Promise6
任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前解析。
作用:
- 允许用户处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
- 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。
Promise.then
Promise.then 也是独立于事件循环之外的,有一个自己的队列,但是优先级要比 process.nextTick 要低,所以当微任务中同时存在 process.nextTick 和 Promise.then 时,会优先值 process.nextTick。
setTimeout(() => {
console.log('setTimeout7')
Promise.resolve().then(() => {
console.log('Promise7')
})
process.nextTick(() => {
console.log('nextTick7')
})
}, 0)
setTimeout(() => {
console.log('setTimeout8')
Promise.resolve().then(() => {
console.log('Promise8')
})
}, 0)
// 输出:
setTimeout7
nextTick7
Promise7
setTimeout8
Promise8
Node.js 于浏览器的时间队列的差异
浏览器环境下,就两个队列,一个宏任务队列,一个微任务队列。微任务的任务队列是在每个宏任务执行完之后执行(清空微任务队列)。
在 Node.js 中,每个任务队列(6个阶段)的每个任务执行完毕之后,就会清空这个微任务队列
结语
如果这篇文章帮到了你,欢迎点赞👍和关注⭐️。
文章如有错误之处,希望在评论区指正🙏🙏。
附: