架构
- Node bindlings: 是C++与JavaScript沟通的桥梁, 封装了V8和Libuv的细节,向上层提供API。
特点
对于I/O密集型的应用,能提供高效的性能
文件读取、网络请求等任务
网络应用
API服务器,实时通信服务
文件操作
对于CPU密集型的任务,会阻塞事件循环,降低性能
复杂的计算和数据处理,通常需要连续的、不间断的CPU时间来执行,将占据整个主线程,导致异步操作无法及时处理,阻塞事件循环,使程序无法及时响应,降低程序的性能
可以使用单独的线程或进程去处理CPU密集型计算,避免阻塞主线程及事件循环
线程:worker_threads(在一个 Node.js 进程中创建多个线程)
进程:child_process(创建新的进程)
集群:cluster(在多个 CPU 核上运行 Node.js 应用)
单线程
- 一个 Node.js 应用只在一个进程上运行,使用一个线程来执行 JavaScript 代码
call stack
Call Stack 和 Execution Context Stack 是同一个东西,即 LIFO 队列,用于存储代码执行期间创建的上下文
- 当调用一个函数时,该函数的返回地址、参数、局部变量都会被压栈(push)。如果当前函数又调用了其他函数,则其他函数的相关数据也会放入调用栈。当函数返回的时候,就会被出栈(pop),返回值被赋值给局部变量。然后该局部变量也会被压栈。
- 当函数执行完后,该函数中的局部变量会被出栈(pop),然后该变量就消失了(仅限number string boolean),如果是对象、数组等的值存储在堆(heap)中。变量只是指向它们的指针,传递变量只是传递的指针,使指针的值在不同的栈帧中可变。当函数从栈中弹出时,只弹出了指向对象的指针,而实际值在堆中,由垃圾回收器回收。
异步非阻塞I/O
异步指异步获取结果,但是代码还是同步执行的
通过回调函数异步获取(Promise本质也是回调,只是解决了回调地狱,提高了代码的可读性)
例如 async方法中的代码、await后面表达式中的代码、Promise传入的executor中的代码,都会同步执行,只是回调函数会进入事件循环异步执行,例如 async方法中await下面行的代码、setTimeout中传入的回调、promise.then方法中传入的回调
通过事件循环实现
事件驱动
主线程执行同步代码的过程中,将异步任务及其回调函数卸载到系统内核
由libuv 通过工作队列、线程池等机制执行异步任务,异步任务完成后,根据异步任务的类型将其回调函数放入事件循环的回调队列中
异步任务完成后才将回调加入队列
主线程空闲时,从事件循环中获取回调函数,压入调用栈(Call Stack)执行
libuv
- node 事件循环底层由 libuv 实现
线程池
默认大小为 4,但可以在启动时通过 UV_THREADPOOL_SIZE 环境变量来更改。增加线程池的大小能够缩短运行异步方法的多次调用的总时间
支持多个异步操作并发执行
例如支持 promise concurrency 的 Promise.all 依赖于线程池实现
工作队列
uv_queue_work
函数1
2
3
4
5
6
7
8
9
10loop = uv_default_loop();
# 获取libuv提供的 默认事件循环对象
uv_queue_work(loop, &req[i], fib, after_fib);
# 执行fib函数,即异步任务
# 执行after_fib函数,即异步任务完成后的回调函数
uv_run(loop, UV_RUN_DEFAULT);
# 以默认模式运行 libuv 的事件循环,开始处理异步操作和回调函数。
# 即 uv_run 函数会一直运行,直到事件循环中没有未完成的任务或调用 uv_stop 停止事件循环将异步任务(fib)放入libuv的工作队列中执行
执行完后,根据异步任务的类型,将回调函数(after_fib)放入事件循环对应的队列中
例如 setImmediate 的回调将放入 check阶段的队列中
先获取事件循环对象,主线程同步代码执行完毕后才运行
事件循环
-
1
uv_loop_t *loop = uv_default_loop()
在单线程上运行
由函数
uv_run
控制,nodejs程序启动后,通过一个 while true 循环,在主线程上运行1
2
3
4
5
6// 整个程序运行逻辑可以按如下方式理解
function main(){
// run synchronous code
uv_run(loop, UV_RUN_DEFAULT); // 同步代码执行完之后才调用 uv_run 开始事件循环
}
main()参考 Does the event loop run on same thread as the JS main thread?
当我们在回调函数里执行 while(1) {}, 整个服务都无法响应了
阶段
- 共6个阶段,每个阶段有自己特定的操作并维护一个FIFO回调队列,执行不同类型的回调
- 事件循环进入到一个阶段时,会执行该阶段特定的操作及队列中的回调,直到队列为空或达到最大执行数量限制。然后进入下一阶段
1 | ┌───────────────────────────┐ |
timers
执行由
setTimeout()
和setInterval()
调度的回调设定的执行时间是最快执行时间,一般会比预期时间要晚
实际执行时间由 poll 阶段控制
进入 poll 阶段,如果队列为空,会等待,直到达到最快timer的阈值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24const fs = require('fs');
function someAsyncOperation(callback) {
// Assume this takes 95ms to complete
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
const startCallback = Date.now();
// do something that will take 10ms...
while (Date.now() - startCallback < 10) {
// do nothing
}
});- 事件循环进入 timer 阶段,没有到达的timer,继续下一阶段
- 进入 poll 阶段,由于
fs.readFile
尚未完成,回调队列为空,因此node将阻塞在该阶段,直到达到最快timer的阈值(即预期等待100ms)。当等待95ms后fs.readFile
完成,回调函数被加入队列中并执行,耗时10ms。回调完成时,队列为空,node将检查是否有达到的timer,发现已经有一个达到,然后回到timer阶段执行其回调,此时timer回调函数在105ms后才执行 - libuv 会限制 poll 阶段的最大等待时间,最大值依赖于系统
pending callback
执行延迟到下一个循环迭代的 I/O 回调,例如一些系统调用错误,比如网络 stream, pipe, tcp, udp通信的错误 callback
大多数情况下,所有 I/O 回调都会在轮询阶段执行。然而,在某些情况下,有些回调会被推迟到下一个循环
如果TCP socket 在尝试连接时收到 ECONNREFUSED 错误,一些*nix系统希望等待报告错误,这些回调会在 pending callback 执行
idle, prepare
- 仅系统内部使用
poll
poll阶段有两个功能:
计算最长阻塞时间并等待传入的回调(连接或请求等)
node会计算最快达到的timer时间,然后阻塞在该阶段
处理 poll 队列的事件
执行除 close callback、timer callback、setImmediate callback 之外的所有回调。
当进入到poll阶段,并且没有设定 timers 时,会发生以下两种情况
poll队列不为空
Event Loop 将同步的执行poll queue里的callback,直到queue为空或者执行的callback到达上限
poll队列为空
- 如果代码设定了
setImmediate()
, Event Loop将结束poll阶段,进入到check
阶段执行setImmediate()
的回调 - 如果代码没有设定
setImmediate()
,Event Loop将阻塞在该阶段,等待回调添加到队列中,然后立即执行
- 如果代码设定了
当进入到poll阶段,并且设定了timers
- 一旦 poll 队列为空,Event Loop会检查是否有已经到达的timer,如果有,Event Loop将回到timer阶段并执行那些timer的callback
check
- 执行
setImmediate()
调度的回调
close callback
执行关闭事件的回调
例如
socket.on('close', ...)
setImmediate vs setTimeout
setTimeout
在指定时间后执行
设置的时间是最快执行时间,实际执行时间要 >= 设定的时间,因为实际执行时间由 poll 阶段控制
setImmediate
在当前循环 poll 阶段后执行
将操作放在下一轮循环中执行
不阻塞当前上下文向 当前微任务队列中 添加的微任务(执行)
1
2
3
4
5
6
7
8
9
10
11
12
13async function tryCallAsync(fn, ...args) {
await new Promise(resolve => {
setImmediate(resolve);
});
return fn(...args);
}
// use setImmediate to ensure all sync logic will run in the next loop
new Promise(resolve => setImmediate(resolve)).then(() => { // 下面的回调将异步执行
Auth.handleCaptchaSuccess(type, account, sendType)
.catch(err => {
logger.error('Fail to handle captcha success', err)
})
})参考
执行顺序
在主模块(非IO循环中)注册
执行顺序不确定,与进程性能有关
1
2
3
4
5
6
7
8// timeout_vs_immediate.js
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});在回调函数中注册
先执行 immediate 再执行 timeout
1
2
3
4
5
6
7
8setTimeout(() => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
}, 0)
浏览器事件循环
区别
实现
- nodejs的event是基于libuv实现
- 浏览器的event loop,在html5的规范中定义,由浏览器厂商实现
任务
宏任务
浏览器 | Node | |
---|---|---|
I/O | ✅ | ✅ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
requestAnimationFrame | ✅ | ❌ |
微任务
浏览器 | Node | |
---|---|---|
process.nextTick | ❌ | ✅ |
MutationObserver | ✅ | ❌ |
Promise.then(catch finally) | ✅ | ✅ |