NodeJS 的事件环
NodeJS 基本认知
-
基于 Chrome V8 引擎的 JS 运行环境
-
JS 可以运行在服务端
-
Node 运行环境只包含 JS 中的 ES 部分、Node 模块和 Node API
-
事件驱动(事件完成通知,异步)
-
非阻塞式 I/O (异步的输入输出)
-
外部依赖包与模块管理器 npm
-
主线程交替处理任务
- 常见服务端:多线程同步模型的高并发能力(高性能处理线程池)
- NodeJS 可以开辟子进程。child_process,cluster
NodeJS 不是一种语言,而是通过 Chrome V8 引擎提供的 JS 运行环境,提供 API 支持服务端运行。
事件驱动
ES Module import、export
CommonJs requrie、module.exports
function test (a, b, cb) {
const res = a + b;
cb && cb(res);
}
test(1, 2, function (res) {
console.log(res);
});
// 事件驱动,通过回调函数的方式通知。事件驱动一般都是异步的。
// 事件驱动:别人做一件事情,完成之后通过一种方式通知你,你可以做下一件事情
非阻塞式 IO
纯函数是标准 IO,一个特定的输入存在相同的输出。
NodeJS 擅长做什么
擅长:I/O 操作,文件、网络、数据库操作
不擅长:CPU 密集型操作,高性能逻辑运算、解压缩、数据分析等操作
具体可以做什么:
- 前后端分离解决跨域
- 作为中间层代理转发
- 服务端渲染
- 组装 HTML,直接返回 HTML
- 前端工程化服务与工具
- webpack 基于 node 实现打包功能
- 文件读取、分析源码、编译源码、编译压缩、打包文件
JS 单线程与多线程对比
JS 主线程是单线程。单线程可以防止多个线程造成 DOM 操作与渲染的任务冲突。
Node 中沿用主线程为单线程的方式。
NodeJS 不存在微任务队列,存在空闲时间,直接把微任务清空。
NodeJS 中宏任务是有分类的,存在不同阶段。
多线程要频繁切换任务上下文处理多个问题,单线程不需要存在任务上下文切换问题。
多线程在处理多个问题时可能需要管锁机制,单线程不需要管锁机制。
// 文件读取就是一个事件
readFile('./demo.json', function (res) {
console.log(res);
});
同步与异步、阻塞与非阻塞
同步:按照顺序往下执行
异步:和顺序无关,和是否执行完、是否得到结果相关
console.log(1);
new Promise(resolve => {
console.log(2);
resolve();
}).then(() => {
console.log(3);
});
console.log(4);
// 1 2 4 3
阻塞是一种现象,同步异步是一种方式。
NodeJS 是异步非阻塞,文件读取过程中可以做其他任务。
同步阻塞
// 1.txt
this is my first text.
// 2.txt
this is my second text.
// index.js
const { readFileSync } = require('fs');
console.log(readFileSync('1.txt', 'utf-8'));
console.log('1.txt');
console.log(readFileSync('2.txt', 'utf-8'));
console.log('2.txt');
异步非阻塞
// index.js
const { readFile } = require('fs');
readFile('1.txt', 'utf-8', function (err, data) {
console.log(data);
});
console.log('1.txt');
readFile('2.txt', 'utf-8', function (err, data) {
console.log(data);
});
console.log('2.txt');
NodeJS IO 操作 建议都使用异步非阻塞的 API。
NodeJS 事件环
NodeJS 主线程还是单线程,事件交于其他线程处理。
- Node 通过事件环机制运行 JS 代码。
- Node 提供线程池处理 I/O 操作任务
- Node 存在两种线程:
- 事件循环线程:负责任务调度 require,同步执行回调、注册新任务
- 线程池(libuv 实现):负责处理任务 I/O 操作、CPU 密集型任务(不擅长)
Node 内核 Libuv 实现了线程池和事件环。
NodeJS 实际上并不存在事件队列,只是讲事件交于线程池处理,处理完成通知主线程进行下一步操作。
事件环阶段:
- Timers:setTimeout/setInterval
- Pending callbacks:执行延迟到下一个事件环迭代的 I/O 回调(内部机制使用)
- IdIe, prepare:系统内部机制使用
- Poll:检查新的 I/O 事件与执行 I/O 事件回调
- Check:setImmediate
- Close callbacks:关闭的回调函数(内部机制使用)
NodeJS 主执行栈执行完代码之后,清空微任务,然后进入事件环阶段。
Timers 阶段意味着执行所有任务,并不代表任务执行完成。
当 setTimeout 和 SetImmediate 同时存在。
如果执行到 Poll 阶段,Timers 中任务已经执行完毕,就会先执行 Timers 阶段中的事件回调,再执行 SetImmediate,
如果执行到 Poll 阶段,Timers 中任务并没有执行完毕,就会先执行 SetImmediate,在执行 Timers 阶段中的事件回调。
NodeJS 事件环案例分析
案例1
const fs = require('fs');
const { readFile } = fs;
// 微任务
Promise.resolve().then(() => {
console.log(1);
});
// 微任务
process.nextTick(() => {
console.log(2);
});
console.log('start');
// Poll
readFile('1.txt', 'utf-8', () => {
setTimeout(() => {
console.log(3);
}, 0);
process.nextTick(() => {
console.log(4);
});
setImmediate(() => {
console.log(5);
});
console.log(6);
});
console.log(7);
// Timers
setTimeout(() => {
console.log(8);
}, 0);
// Check
setImmediate(() => {
console.log(9);
});
console.log('end');
// 主执行栈:start、7、end
// 清空微任务:2、1 (nextTick 优先于 promise 执行)
// 事件环:8、9 or 9、8 (Timers 如果先执行完,就会先输出 8,反之先输出 9)
// 主执行栈:6
// 清空微任务:4
// 事件环:5、3(IO 中,setImmediate 优先于 setTimeout)
案例2
Node 10 及以下版本和 Node 11 主要区别:
- Node 10 及以下版本会在切换阶段的时候清空微任务;
- Node 11 及以上版本会在宏任务执行完毕或者切换阶段时,清空微任务。
const fs = require('fs');
const { readFile } = fs;
process.nextTick(() => {
console.log(1);
});
console.log('start');
setTimeout(() => {
console.log(2);
}, 0);
setTimeout(() => {
console.log(3);
}, 0);
setImmediate(() => {
console.log(4);
process.nextTick(() => {
console.log(5);
Promise.resolve().then(() => {
console.log(6);
});
})
});
readFile('1.txt', 'utf-8', () => {
process.nextTick(() => {
console.log(7);
});
setTimeout(() => {
console.log(8);
}, 0);
setImmediate(() => {
console.log(9);
});
});
readFile('2.txt', 'utf-8', () => {
process.nextTick(() => {
console.log(10);
});
setTimeout(() => {
console.log(11);
}, 0);
setImmediate(() => {
console.log(12);
});
});
console.log('end');
// 主执行栈:start、end
// 微任务:1
// 事件环:2、3、4 or 4、2、3
// 微任务:5、6
// 事件环
// 微任务:7、10(读取速度一致时)
// 事件环:9、12、8、11(读取速度一致时)