重学nodejs系列之web & node eventLoop(一)
引言
nodejs是一个基于事件驱动和非阻塞I/O的js运行时环境。
事件驱动由下面四个部分组成:
- 事件(Event): 事件是程序中某一特定瞬间发生的事情,可以是用户的操作、系统状态的变化等。在Node.js中,事件可以是网络请求、文件读取完成等。
- 事件监听器(Event Listener): 事件监听器是一个函数,它负责处理特定类型的事件。在Node.js中,监听器通过
on
方法绑定到特定的事件上。 - 触发器(Emitter): 事件的发起者被称为触发器或事件发射器。在Node.js中,事件触发器是一个对象,通过调用特定的方法(比如
emit
方法)来触发与之相关联的事件。 - 回调函数(Callback): 事件处理程序通常是回调函数,它们在相应的事件发生时被异步调用。回调函数负责处理事件,并在事件完成后执行相关的操作。
而非阻塞I/O是一种处理输入输出操作的方式,允许程序在等待数据准备好时继续执行其他任务,而不被阻塞。关键概念包括:
- 异步(Asynchronous): 非阻塞I/O操作是异步进行的,不会等待操作完成,而是通过轮询或事件通知的方式检查状态。
- 轮询或事件驱动: 程序通过轮询或事件驱动的方式检查I/O操作是否完成,一旦数据准备好,再进行相应的处理。
理解枯燥的概念是无聊的,我们来看看在nodejs中实际的例子
import fs from "fs"
import { EventEmitter } from "events"
import path from "path"
// 创建一个事件触发器
const eventEmitter = new EventEmitter();
// 绑定事件监听器
eventEmitter.on('fileRead', (data) => {
console.log(`文件内容:${data}`);
});
// 触发文件读取事件
fs.readFile(path.join(__dirname, "test.txt"), 'utf8', (err, data) => {
if (err) throw err;
// 触发事件,并传递读取的数据
eventEmitter.emit('fileRead', data);
});
console.log('程序继续执行,不会等待文件读取完成。');
/**
程序继续执行,不会等待文件读取完成。
文件内容:事件驱动由下面四个部分组成:
事件(Event): 事件是程序中某一特定瞬间发生的事情,可以是用户的操作、系统状态的变化等。在Node.js中,事件可以是网络请求、文件读取完成等。
事件监听器(Event Listener): 事件监听器是一个函数,它负责处理特定类型的事件。在Node.js中,监听器通过on方法绑定到特定的事件上。
触发器(Emitter): 事件的发起者被称为触发器或事件发射器。在Node.js中,事件触发器是一个对象,通过调用特定的方法(比如emit方法)来触发与之相关联的事件。
回调函数(Callback): 事件处理程序通常是回调函数,它们在相应的事件发生时被异步调用。回调函数负责处理事件,并在事件完成后执行相关的操作。
**/
import http from "http"
// 发起一个异步的HTTP请求
const request = http.get('http://www.baidu.com', (response) => {
let data = '';
// 当有数据到达时触发data事件
response.on('data', (chunk) => {
data += chunk;
});
// 当数据接收完成时触发end事件
response.on('end', () => {
console.log(`收到的数据:${data}`);
});
});
// 请求不会阻塞,程序可以继续执行其他任务
console.log('程序继续执行,不会等待HTTP请求完成。');
再谈web eventLoop
因为JS是单线程的,浏览器里面也有一个eventLoop
。浏览器要处理用户交互、网络请求、定时器、DOM渲染等事件。事件循环使得浏览器能够同时处理多个任务而不阻塞主线程。
Web Event Loop通常被划分为不同的阶段:
- 宏任务队列(Macrotask Queue): 包括整体的script代码、setTimeout、setInterval等。
- 微任务队列(Microtask Queue): 包括Promise、MutationObserver等。
执行顺序简单如下:
- 执行同步代码
- 执行微任务队列中的所有任务
- 执行当前宏任务队列中的一个任务
- 重复执行2 3步骤,直至所有队列为空
console.log('开始');
setTimeout(() => {
console.log('setTimeout回调');
}, 0);
const fetchData = () => {
return new Promise((resolve, reject) => {
console.log('请求数据');
setTimeout(() => {
resolve('请求成功');
}, 1000);
});
};
const processPromise = async () => {
console.log('执行promise之前');
const result = await fetchData(); // 等待promise被执行成功才执行后面代码,其余任务不会被阻塞
console.log(result);
console.log('执行promise之后');
};
processPromise();
console.log('结束');
// 开始 执行promise之前 请求数据 结束 setTimeout回调 请求成功 执行promise之后
好的,简单回顾了一下web eventLoop我们就开始这篇文章的主题了
node eventLoop
nodejs把任务分为大致如下阶段:
- Timers(定时器阶段): 处理通过
setTimeout
和setInterval
设置的定时器回调函数。在这个阶段,Node.js会检查是否有定时器到期,并执行相应的回调。 - I/O callbacks(I/O回调阶段): 处理I/O操作的回调函数,比如文件读取、网络请求等。当一个异步I/O操作完成时,其回调会在这个阶段执行。
- Idle, prepare(空闲阶段,准备阶段): 这两个阶段通常被忽略,不过Node.js Event Loop在进入下一个阶段之前会执行一些内部操作。
- Poll(轮询阶段): 负责处理轮询队列中的事件,比如处理TCP连接、接收新的连接、以及执行
setImmediate
的回调函数。 - Check(检查阶段): 处理通过
setImmediate
注册的回调函数。在Poll阶段完成后,会执行Check阶段中的回调函数。 - Close callbacks(关闭回调阶段): 处理通过
socket.on('close', ...)
等事件注册的回调函数。在TCP连接关闭时,这个阶段会执行相应的回调。
按宏任务和微任务分:
- 宏任务队列(Macrotask Queue):
setTimeout
和setInterval
,I/O 操作的回调,例如文件读取、网络请求,一些特殊的任务,例如setImmediate
回调。 - 微任务队列(Microtask Queue): Promise 的回调、
process.nextTick
等。
nodejs的执行顺序简单如下:
- 先执行同步代码
- 执行所有微任务
- 执行宏任务队列的一个任务
- 重复2 3步骤,直至宏任务队列为空
import fs from "fs"
import path from "path"
console.log('开始执行');
// 定时器任务,属于宏任务
setTimeout(() => {
console.log('Timeout');
}, 0);
// I/O 操作的回调,属于宏任务
fs.readFile(path.join(__dirname, 'test.txt'), 'utf8', (err, data) => {
console.log('fs回调');
});
// 特殊任务,属于宏任务
setImmediate(() => {
console.log('Immediate');
});
// 微任务
Promise.resolve()
.then(() => {
console.log('Promise 1');
})
.then(() => {
console.log('Promise 2');
});
// process.nextTick 回调,属于微任务
process.nextTick(() => {
console.log('process.nextTick');
});
console.log('结束');
// 开始执行 结束 process.nextTick Promise1 Promise2 Timeout Immediate fs回调
setTimeout & setImmediate & process.nextTick
setTimeout
、setImmediate
和 process.nextTick
是 Node.js 中用于处理异步操作的三种机制。它们在事件循环中有不同的执行时机和优先级。
setTimeout:
- 使用
setTimeout
可以延迟执行一个函数,它将在指定的时间间隔之后添加一个宏任务到宏任务队列。 - 由于存在最小延迟时间(通常是4毫秒),实际执行时间可能会略有延迟。
setTimeout(() => {
console.log('setTimeout');
}, 1000);
setImmediate:
setImmediate
用于在当前事件循环迭代的末尾添加一个宏任务。setImmediate
的回调会在当前宏任务执行完毕后、下一个宏任务执行之前执行。
setImmediate(() => {
console.log('setImmediate');
});
process.nextTick:
process.nextTick
会在当前事件循环迭代的末尾立即执行,优先级高于微任务队列中的Promise回调。- 适用于确保在下一个事件循环之前执行某些代码。
process.nextTick(() => {
console.log('process.nextTick');
});
setTimeout(() => {
console.log('setTimeout');
}, 1000);
setImmediate(() => {
console.log('setImmediate');
});
process.nextTick(() => {
console.log('process.nextTick');
});
// process.nextTick setImmediate setTimeout
- setTimeout 在timer阶段执行。
- setImmediate 在check阶段执行。
- process.nextTick 在当前阶段的末尾执行,不属于特定的阶段。
我们来看一道题目
console.log("开始");
setTimeout(() => {
console.log("Timeout 1");
}, 0);
setTimeout(() => {
console.log("Timeout 2");
}, 100);
setImmediate(() => {
console.log("Immediate 1");
});
process.nextTick(() => {
console.log("Next Tick 1");
});
console.log("结束");
// 开始 结束 Next Tick 1 Timeout 1 Immediate 1 Timeout 2
- 开始 和 结束 首先被打印,因为它们是同步代码,按照书写顺序执行。
- Next Tick 1 在事件循环的下一个循环中被执行,因为 process.nextTick 的回调函数在当前循环结束后立即执行。// 不属于任何阶段
- Timeout 1 在下一个循环中被执行,尽管设置了0毫秒的延迟,但由于事件循环的特性,它会在下一个循环中执行。 // timer阶段
- Immediate 1 在当前循环的末尾被执行,因为setImmediate的回调函数在当前循环结束时执行。// check阶段
- Timeout 2 在由于上述操作而产生的新循环中被执行,由于设置了100毫秒的延迟,因此在 Timeout 1 之后执行。 // timer阶段
转载自:https://juejin.cn/post/7332014308933943322