likes
comments
collection
share

重学nodejs系列之web & node eventLoop(一)

作者站长头像
站长
· 阅读数 36

引言

nodejs是一个基于事件驱动和非阻塞I/O的js运行时环境。

事件驱动由下面四个部分组成:

  1. 事件(Event): 事件是程序中某一特定瞬间发生的事情,可以是用户的操作、系统状态的变化等。在Node.js中,事件可以是网络请求、文件读取完成等。
  2. 事件监听器(Event Listener): 事件监听器是一个函数,它负责处理特定类型的事件。在Node.js中,监听器通过on方法绑定到特定的事件上。
  3. 触发器(Emitter): 事件的发起者被称为触发器或事件发射器。在Node.js中,事件触发器是一个对象,通过调用特定的方法(比如emit方法)来触发与之相关联的事件。
  4. 回调函数(Callback): 事件处理程序通常是回调函数,它们在相应的事件发生时被异步调用。回调函数负责处理事件,并在事件完成后执行相关的操作。

而非阻塞I/O是一种处理输入输出操作的方式,允许程序在等待数据准备好时继续执行其他任务,而不被阻塞。关键概念包括:

  1. 异步(Asynchronous): 非阻塞I/O操作是异步进行的,不会等待操作完成,而是通过轮询或事件通知的方式检查状态。
  2. 轮询或事件驱动: 程序通过轮询或事件驱动的方式检查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等。

执行顺序简单如下:

  1. 执行同步代码
  2. 执行微任务队列中的所有任务
  3. 执行当前宏任务队列中的一个任务
  4. 重复执行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把任务分为大致如下阶段:

  1. Timers(定时器阶段): 处理通过setTimeoutsetInterval设置的定时器回调函数。在这个阶段,Node.js会检查是否有定时器到期,并执行相应的回调。
  2. I/O callbacks(I/O回调阶段): 处理I/O操作的回调函数,比如文件读取、网络请求等。当一个异步I/O操作完成时,其回调会在这个阶段执行。
  3. Idle, prepare(空闲阶段,准备阶段): 这两个阶段通常被忽略,不过Node.js Event Loop在进入下一个阶段之前会执行一些内部操作。
  4. Poll(轮询阶段): 负责处理轮询队列中的事件,比如处理TCP连接、接收新的连接、以及执行setImmediate的回调函数。
  5. Check(检查阶段): 处理通过setImmediate注册的回调函数。在Poll阶段完成后,会执行Check阶段中的回调函数。
  6. Close callbacks(关闭回调阶段): 处理通过socket.on('close', ...)等事件注册的回调函数。在TCP连接关闭时,这个阶段会执行相应的回调。

按宏任务和微任务分:

  • 宏任务队列(Macrotask Queue): setTimeoutsetInterval,I/O 操作的回调,例如文件读取、网络请求,一些特殊的任务,例如 setImmediate 回调。
  • 微任务队列(Microtask Queue): Promise 的回调、process.nextTick 等。

nodejs的执行顺序简单如下:

  1. 先执行同步代码
  2. 执行所有微任务
  3. 执行宏任务队列的一个任务
  4. 重复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

setTimeoutsetImmediateprocess.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
  1. 开始 和 结束 首先被打印,因为它们是同步代码,按照书写顺序执行。
  2. Next Tick 1 在事件循环的下一个循环中被执行,因为 process.nextTick 的回调函数在当前循环结束后立即执行。// 不属于任何阶段
  3. Timeout 1 在下一个循环中被执行,尽管设置了0毫秒的延迟,但由于事件循环的特性,它会在下一个循环中执行。 // timer阶段
  4. Immediate 1 在当前循环的末尾被执行,因为setImmediate的回调函数在当前循环结束时执行。// check阶段
  5. Timeout 2 在由于上述操作而产生的新循环中被执行,由于设置了100毫秒的延迟,因此在 Timeout 1 之后执行。 // timer阶段
转载自:https://juejin.cn/post/7332014308933943322
评论
请登录