【Node.js】线程和线程池,事件循环,事件驱动架构,流,模块
Node.js 架构
Node.js是一个JavaScript语言运行时(Runtime)
- 用户可以在浏览器之外的环境使用JavaScript
- 可以使用javascript代码实现后端开发
Chrome v8(C++,js)
将js code转化为machine code
libuv(C++)
focus on asynchronous I/O,与计算机操作系统、文件系统、网络等关联。事件循环与线程池由它实现。事件循环解决简单的回调函数,Network IO;线程池:do heavy works如获取文件,压缩等。
下面四个库,大概了解
线程、线程池
大概理解一下进程和线程:
一个正在运行的程序就是一个进程(process),Node.js运行时就会开启一个进程;一个指令序列就是一个线程(thread),我们的code就在这里运行。
单线程+事件循环
- top level code:不在任何回调函数中的代码
- event loop :事件循环,大部分应用的工作在这里实现
- heavy/expensive tasks:一些会使事件循环阻塞的代码
事件循环干了什么
- receive events :当事件完成时,它会接收到完成的信号
- call their callback functions:这些callbacks调用的顺序?
- offload heavy tasks
事件循环的阶段
事件循环有很多阶段,每个阶段有自己的回调队列。队列中的回调函数依次执行,直到队列中没有回调函数,事件循环进入下一阶段。区分:一次循环(a circle,a tick),一个阶段(phase)。
- 超时的计时器
- 如果在其他阶段计时器超时了,它会等到下一次循环的此阶段执行callback
- 网络或文件请求:polling = looking for new I/O event
- 特殊的立即执行计时器
- 关闭回调
- 两个特殊的阶段:
- nextTick和微任务队列——主要解决成功的Promise,这两个队列中若有回调要执行,会直接在当前阶段之后立即执行,而不必等到下一次循环。
何时退出循环?轮询有无pending的timer 或 I/O :如我们的.listen(),就使得程序保持监听是否有 HTTP请求 ,而非退出。
不要写阻塞的代码
事件循环代码说明
1.没有异步在回调函数里(没有事件循环)
const fs = require('fs')
setTimeout(() => console.log("Timer 1 finished"), 0);
setImmediate(() => console.log("Immediate 1 finished"));
fs.readFile("test-file.txt", () => {
console.log("I/O finished");
});
console.log("Hello from the top level code")
输出:
Hello from the top level code
Timer 1 finished
Immediate 1 finished
I/O finished
2.事件循环的阶段
//...同上
fs.readFile("test-file.txt", () => {
console.log("I/O finished");
console.log('------ Event Loop ------');
setTimeout(() => console.log("Timer 2 finished"), 0);
setTimeout(() => console.log("Timer 3 finished"), 3000);
setImmediate(() => console.log("Immediate 2 finished"));//注意这句执行的位置
process.nextTick(()=>console.log("Process.nextTick"));//立即执行
});
//...
输出:
- 进入event loop,nextTick()立即执行,虽然它叫next tick,但实际上它是在next loop phase执行的,注意与setImmediate()区别,我们通常在程序中使用后者
- Immediate 2 先执行,setTimeout 2 后执行?程序在I/O polling 阶段暂停并等待事件,当该阶段回调队列为空,继续按顺序执行,所以即便setTimeout设置了0s,也在Immediate 2之后执行
- 3s后Timer 3执行,之后退出程序
Hello from the top level code
Timer 1 finished
Immediate 1 finished
I/O finished
------ Event Loop ------
Process.nextTick
Immediate 2 finished
Timer 2 finished
Timer 3 finished
3.事件循环自动卸载heavy task到线程池,以crypto为例
const crypto = require("crypto");
const start = Date.now();
process.env.UV_THREADPOOL_SIZE = 4;//线程池大小
fs.readFile("test-file.txt", () => {
//...
/*同步版本*/
crypto.pbkdf2Sync("password", "salt", 100000, 1024, "sha512");
console.log(Date.now() - start, "Password encrypted");//...*4
/*异步版本*/
crypto.pbkdf2("password", "salt", 100000, 1024, "sha512",()=>{
console.log(Date.now() - start, "Password encrypted");//...*4
})
});
console.log("Hello from the top-level code");
同步调用,阻塞。输出:
------ Event Loop ------
972 Password encrypted
1916 Password encrypted
2842 Password encrypted
3764 Password encrypted
Process.nextTick
Immediate 2 finished
Timer 2 finished
Timer 3 finished
异步,线程池大小=4 ,输出:
------ Event Loop ------
Process.nextTick
Immediate 2 finished
Timer 2 finished
2502 Password encrypted
2556 Password encrypted
2592 Password encrypted
2624 Password encrypted
Timer 3 finished
线程池大小=2 ,输出:
------ Event Loop ------
Process.nextTick
Immediate 2 finished
Timer 2 finished
2793 Password encrypted
2967 Password encrypted
Timer 3 finished
3542 Password encrypted
3567 Password encrypted
事件驱动架构/观察者模式
Event emitter 代码说明
自定义事件
const EventEmitter = require("events");
class Sales extends EventEmitter {
constructor() {
super();
}
}
const myEmitter = new Sales();
//观察者Event Listener
myEmitter.on("newSale", stock => {
console.log(`There are now ${stock} items left in stock.`);
});
//Event emitter
myEmitter.emit("newSale", 9);
使用build-in模块
const http = require("http");
const server = http.createServer();
//Event Listener
server.on("request", (req, res) => {
console.log("Request received!");
console.log(req.url);
res.end("Request received");
});
server.on("request", (req, res) => {
console.log("Another request 😀");
});
server.on("close", () => {
console.log("Server closed");
});
//Event emitter
server.listen(8000, "127.0.0.1", () => {
console.log("Waiting for requests...");
});
流
是Event emitter的实例
流媒体,你在看的时候,视频文件其实不是全部都下载好的。
四种流类型
比如,我们想从本地服务器获取一个非常大的文件发送到客户,如果采用之前的readFile方法: 我们要将整个文件读取完,用变量储存到内存,再发送,会是一个非常缓慢的过程,并且Node进程会很快用完内存,我们的应用会崩溃。只有当本地文件很小时readFile方法是适用的。如果采用流操作:
- 事件1 data:readable.on开启流动,数据会不断触发data事件
- 事件2 end: 监听到文件读取完毕,会自动触发end事件
const readable = fs.createReadStream("test-file.txt");
readable.on("data", chunk => {
res.write(chunk);
});
readable.on("end", () => {
res.end();
});
readable.on("error", err => {
console.log(err);
res.statusCode = 500;//server error
res.end("File not found!");
});
如果可读流从磁盘读入的速度,远远大于写入响应的速度,即接收到的这一波数据还来不及发送,下一波就来了,这会造成内存溢出,即背压Backpressure
解决背压问题——pipe
可读流的方法。原理是控制输入输出数据的速度:
const readable = fs.createReadStream("test-file.txt");
readable.pipe(res);
// readableSource.pipe(writeableDest)
模块
require模块时发生了什么
去哪找——路径?
IIFE function
每个node.js运行时环境,一个 Node.js 文件就是一个模块。我们所有的代码其实都被包裹在一个立即执行函数中,在.js文件中直接console.log(arguments)
看输出的类数组,就是如下所示的参数序列。
缓存
优先从文件模块的缓存中加载已经存在的模块:
转载自:https://juejin.cn/post/7090432871298498590