Node.js - 事件循环:揭开异步JavaScript背后的魔法

你好,未来的编程巫师们!今天,我们将踏上一次激动人心的旅程,探索Node.js的核心——事件循环(Event Loop)。如果你之前从未写过一行代码,也不用担心;我将作为你的友好向导,带你去了解这个迷人的世界。在本教程结束时,你将理解Node.js是如何同时处理许多事情的,就像你一边做作业、一边看Netflix、一边给朋友发短信一样!

Node.js - Event Loop

事件循环是什么?

想象你是一家繁忙餐厅的厨师。你同时烹饪着多道菜,计时器在滴答作响,订单也在不断进来。你如何在不烧焦食物的同时,也让顾客等待时间最短呢?这正是Node.js的Event Loop为Node.js所做的事情!

事件循环就像一位总厨师,不断地检查哪些事情需要关注,确保一切运行顺畅。它是Node.js能够执行非阻塞I/O操作的秘诀,尽管JavaScript是单线程的。

关键概念

在我们深入探讨之前,让我们先熟悉一些关键概念:

  1. 单线程:JavaScript在单个线程上运行,这意味着它一次只能做一件事情。
  2. 非阻塞:Node.js可以处理多个操作,而无需等待每个操作完成后再继续下一个。
  3. 异步:任务可以现在开始,稍后完成,允许在此期间运行其他代码。

事件循环是如何工作的?

让我们将事件循环分解为以下几个容易理解的步骤:

  1. 执行调用栈中的同步代码
  2. 检查定时器(setTimeout, setInterval)
  3. 检查挂起的I/O操作
  4. 执行setImmediate回调
  5. 处理'close'事件

现在,让我们通过一些代码示例来看看它是如何工作的!

示例1:同步代码与异步代码

console.log("First");

setTimeout(() => {
console.log("Second");
}, 0);

console.log("Third");

你认为输出会是什么?让我们分析一下:

  1. "First"会被立即记录。
  2. 遇到setTimeout,但Node.js不会等待,而是设置一个定时器然后继续。
  3. "Third"被记录。
  4. 事件循环检查完成的定时器并执行回调,记录"Second"。

输出:

First
Third
Second

惊讶吗?这展示了Node.js如何在不阻塞主线程的情况下处理异步操作。

示例2:多个定时器

setTimeout(() => console.log("Timer 1"), 0);
setTimeout(() => console.log("Timer 2"), 0);
setTimeout(() => console.log("Timer 3"), 0);

console.log("Hello from the main thread!");

在这个示例中,我们设置了多个延迟为0毫秒的定时器。但是,事件循环仍然会在主线程完成后处理它们。

输出:

Hello from the main thread!
Timer 1
Timer 2
Timer 3

事件循环的阶段

现在我们已经看到了事件循环的实际运行,让我们更详细地探索它的各个阶段:

1. 定时器阶段

此阶段执行由setTimeout()和setInterval()计划的回调。

setTimeout(() => console.log("I'm a timer!"), 100);
setInterval(() => console.log("I repeat every 1 second"), 1000);

2. 挂起回调阶段

在这个阶段,事件循环执行延迟到下一次循环迭代的I/O回调。

3. 空闲、准备阶段

仅供内部使用。这里没有什么可看的!

4. 轮询阶段

检索新的I/O事件并执行与I/O相关的回调。

const fs = require('fs');

fs.readFile('example.txt', (err, data) => {
if (err) throw err;
console.log(data);
});

5. 检查阶段

在此阶段调用setImmediate()回调。

setImmediate(() => console.log("I'm immediate!"));

6. 关闭回调阶段

一些关闭回调,例如socket.on('close', ...),在此阶段处理。

一切结合在一起

让我们创建一个更复杂的示例,它利用了事件循环的不同方面:

const fs = require('fs');

console.log("Start");

setTimeout(() => console.log("Timeout 1"), 0);
setImmediate(() => console.log("Immediate 1"));

fs.readFile('example.txt', (err, data) => {
console.log("File read complete");
setTimeout(() => console.log("Timeout 2"), 0);
setImmediate(() => console.log("Immediate 2"));
});

console.log("End");

执行顺序可能会让你感到惊讶:

  1. "Start"和"End"被立即记录。
  2. 第一个setTimeout和setImmediate被排队。
  3. 文件读取操作开始。
  4. 事件循环开始其周期:
  • 第一个setTimeout回调执行。
  • 第一个setImmediate回调执行。
  • 当文件读取完成时,其回调被执行。
  • 在文件读取回调内,另一个setTimeout和setImmediate被排队。
  • 第二个setImmediate在第二个setTimeout之前执行。

常见事件循环方法

以下是在Node.js中与事件循环相关的一些常见方法:

方法 描述
setTimeout(callback, delay) 在延迟毫秒数后执行回调
setInterval(callback, interval) 每隔间隔毫秒数重复执行回调
setImmediate(callback) 在下一次事件循环迭代中执行回调
process.nextTick(callback) 将回调添加到“下一个滴答队列”中,在当前操作完成后立即处理

结论

恭喜你!你刚刚迈入了Node.js及其事件循环的迷人世界。记住,就像学习骑自行车一样,掌握异步编程需要练习。如果一开始没有立即理解,不要气馁——继续尝试,很快你就能像专业人士一样编写非阻塞代码!

在我们结束之前,这里有一个有趣的类比:将事件循环想象成旋转木马。不同的任务(如定时器、I/O操作和即时回调)就像试图跳上旋转木马的孩子。事件循环不断地旋转,按照特定的顺序接手和放下任务,确保每个人都得到轮到自己的机会,而木马永远不会停止。

继续编程,保持好奇心,记住——在Node.js的世界里,耐心不仅仅是一种美德,它还是一个回调!

Credits: Image by storyset