阅读视图

发现新文章,点击刷新页面。

Node.js入门:Node.js 事件循环

大家好,我是前端理想哥!

今天是我们Node.js学习的第五节课,这节课我们来聊一聊Node.js 中一个非常重要的概念:事件循环。很多人听过它,但一提到具体原理就有点懵。别担心,今天我带你从代码角度,通俗易懂地理解事件循环的工作原理。

一、什么是事件循环?

事件循环(Event Loop)是 Node.js 能够执行非阻塞 I/O 操作的核心。尽管 Node.js 只有一个主线程,但通过将 I/O 操作交给操作系统的内核处理,Node.js 能够高效地进行并发操作。

举个例子:你可以把事件循环想象成一个老板,老板有很多任务要完成。每当有新的任务出现,老板就会安排合适的员工去做。当员工完成工作后,老板会拿到结果,再继续安排新任务。这样,老板始终能在最短的时间内处理最多的任务。

二、事件循环的6个阶段

因为Node.js底层使用的语言是libuv,一个c++语言,它用来操作底层的操作系统,然后封装了操作系统的接口,Node 的事件循环也是libuv写的,所以Node的生命周期和浏览器的还是有区别的。

因为Node是和操作系统打交道的,所以它的事件循环更加复杂。

Node.js的事件循环有6个主要阶段,执行原理也很简单,这6个阶段,每个阶段都维护了一个事件队列,当到达一个队列后,会检查队列内是否有任务(也就是看下是否有回调函数)需要执行。如果有,就依次执行,直到全部执行完毕、清空队列。

如果没有,那么就会进入到下一个队列继续检查,直到把6个阶段的队列都检查一遍,算是一个轮询。

接下来,让我们逐一了解这些阶段:

1. timers 计时器

这一阶段主要是用来存放计时器的,像 setTimeout、setInterval等的回调函数

举个例子:

setTimeout(() => {
  console.log("这是一个定时器回调");
}, 100);

定时器回调将在指定的时间之后执行, 但由于事件循环的机制,实际的执行时间可能会有所延迟。

2. pending callbacks 等待回调

这个阶段会处理那些在事件循环的其他阶段中未能及时执行的回调。这些回调通常是一些 I/O 操作的结果,比如网络请求失败、文件读取错误等。该阶段的回调并不依赖于定时器,也不属于其他阶段的任务,而是排队等待在此阶段执行。

举个例子:

const fs = require('fs');

// 异步读取文件
fs.readFile('example.txt', (err, data) => {
  if (err) {
    console.error('Error reading file');
  } else {
    console.log('File content:', data.toString());
  }
});

console.log('File reading initiated...');

解释:

  1. fs.readFile 是异步操作,当事件循环进入 poll 阶段时,文件的读取操作会被排入等待执行的队列中。

  2. 如果文件读取完成后,回调函数就会被推送到 pending callbacks 阶段,然后执行该回调输出文件内容。

  3. console.log('File reading initiated...') 是同步代码,优先执行。

执行顺序:

  1. 打印:File reading initiated...

  2. 在 pending callbacks 阶段执行 fs.readFile 的回调,并打印文件内容。

3. idle, prepare

这两个阶段是Node.js内部使用的阶段,在外部应用开发中我们很少接触到,主要是在事件循环的初期做一些准备工作,主要干两件事:

  • 空闲检测(Idle) :检查是否有任何可以执行的操作或者是否进入空闲状态,判断当前事件循环是否需要继续。
  • 内部准备(Prepare) :为后续阶段(尤其是轮询阶段 poll)的事件处理做准备。这个阶段可以看作是事件循环的一个内部“预处理”阶段,它对外部开发者没有直接的影响。

4. poll 轮询

在这个阶段,主要作用是处理 I/O 操作等待新的事件,并决定是否需要进入其他阶段(例如 timers 或 check 阶段)。

具体的流程有四步:

  1. 检索新的 I/O 事件:

poll 阶段会检查是否有新的 I/O 操作完成。例如,网络请求、文件操作、数据库查询等。如果有这些操作完成,它会把相关的回调放入事件队列,并在 poll 阶段执行它们。

  1. 执行已排队的 I/O 回调:

在 poll 阶段,Node.js 会执行所有排队的 I/O 回调,直到队列为空或者达到系统的最大回调数量限制。

  1. 阻塞等待:

如果没有新的 I/O 事件,poll 阶段会阻塞,等待新的事件发生。在此期间,Node.js 会挂起当前线程,直到有事件可以处理。这样避免了 CPU 空转,提高了资源利用效率。

  1. 决定是否跳到 check timers 阶段:

如果没有 I/O 事件可处理,poll 阶段会直接跳到 check 阶段去执行 setImmediate() 调度的回调,或者跳回到 timers 阶段去执行定时器回调。

举个例子:

假设我们有一个 Node.js 程序,监听一个文件变化,并在文件发生变化时触发回调。

const fs = require('fs');

// 使用 fs.watch 监听文件变化
fs.watch('somefile.txt', (eventType, filename) => {
  console.log(`File changed: ${filename}`);
});

console.log('Waiting for file change...');

我们从Node事件循环的角度来分析下这段代码的执行:

    1. Node.js 启动事件循环后,进入 poll 阶段。
    1. fs.watch() 是一个 I/O 操作,它会在底层操作系统等待文件变化的事件。
    1. 当文件发生变化时,操作系统会通知 Node.js 内核,Node.js 会把文件变化事件排入事件队列。
    1. 在 poll 阶段,Node.js 检测到文件变化事件,并执行回调 console.log()。

5. check

这个阶段的作用是执行通过 setImmediate() 调度的回调。这个阶段通常在 poll 阶段之后执行,主要负责执行那些被标记为“立即执行”的回调。

举个例子:

假设你使用 setImmediate() 调度了一个回调函数:

setImmediate(() => {
  console.log('这是setImmediate回调');
});

这个回调函数会在事件循环进入 check 阶段时执行,无论 poll 阶段是否还有事件未处理。setImmediate() 的回调总是会在轮询阶段结束后立即执行。

6. close callbacks

这个阶段用于处理与资源关闭相关的回调,主要是处理如网络连接、文件句柄或其他需要清理和关闭的资源。

举个例子:

const fs = require('fs');

// 打开一个文件流
const stream = fs.createWriteStream('example.txt');

// 写入数据
stream.write('Hello, world!');

// 监听 'close' 事件
stream.on('close', () => {
  console.log('文件已关闭');
});

// 关闭文件流
stream.end();  // 这时会触发 'close' 事件,并执行回调

在这个例子中,当调用 stream.end() 关闭文件流时,'close' 事件会被触发,然后回调函数会在 close callbacks 阶段执行,输出 '文件已关闭'。

三、关键概念:

在Node时间循环中,有几个关键概念:

  1. setImmediate()setTimeout()有什么不同?
  2. process.nextTick() 是什么?为什么要使用 process.nextTick()
  3. process.nextTick()setImmediate()有什么不同?

这些都是关键概念,因为篇幅有限,我们将放在下节视频,一个个给大家讲清楚。

四、总结

Node.js 的事件循环机制让它能够高效地处理 I/O 操作和异步任务。通过合理使用 process.nextTick()、setImmediate() 和 setTimeout(),我们可以精确控制回调的执行时机,避免阻塞主线程。

对于开发者来说,理解事件循环的工作原理非常重要。它不仅能帮助我们优化性能,还能避免一些常见的代码陷阱。

今天的分享就到这里,大家如果有任何问题,欢迎在评论区留言,我们一起讨论!别忘了点赞、收藏、关注哦!下次见!

如果你也对 Node.js 感兴趣,记得关注理想哥,我们一起深入探索。

❌