Node.js頂級1%工程師:精通事件循環
在我们努力成为顶级1% Node.js工程师的过程中,了解事件循环非常重要。本文将深入了解事件循环,包括Promise.all
、Promise.allSettled
等概念,以及它们是如何与事件循环交互的。
本文假设读者对v8的事件循环机制有一定了解。在本文的最后一节中,我推荐了两个很棒的资源,适合所有想开始学习或更深入研究这个主题的人。
照片由 Angely Acevedo 拍摄,来自 Unsplash
事件循环入门在 JavaScript 中,当你调用一个函数时,它会被加入事件队列。V8 引擎会按照加入的顺序来执行这些函数。这构成了 JavaScript 单线程同步执行的基础。这被称为事件循环,因为它会不断检查并执行新的事件。
异步操作和事件循环机制简介当调用一个异步函数时,执行跳转到该异步函数,并将指向当前代码位置的指针添加到事件队列中。V8 继续执行代码,完成后从事件队列中获取下一个事件。当执行到达该指针时,会跳回原函数继续执行。这种架构让 V8 引擎能够并行运行 JavaScript 代码。
Promise.all 和 Promise.allSettled(这两个方法在JavaScript中用于处理多个Promise对象的集合。Promise.all
会等待所有Promise完成并返回一个包含结果的数组,而 Promise.allSettled
不管Promise成功或失败都会等待所有Promise完成并返回一个包含每个Promise最终状态的对象数组。)
Promise.all
和 Promise.allSettled
会将作为参数传入的每个Promise同时添加到消息队列中。如果你依次调用异步函数(并等待每个完成),只有第一个会被添加到消息循环中。下一个函数会在前一个执行完毕后才被添加到消息循环。
有了这些知识,我们可以看到即使在写看似同步的代码时,也有可能跳出同步模式。如果不谨慎处理,这可能会导致意想不到的结果。为了避免这种情况,我建议使用require-await eslint规则(这是一个eslint规则)。
这个例子展示了在 Node.js 中,即使没有任何实际的 I/O 操作,异步处理也可能导致意外的结果。我们来拆解一下:
- 我们定义了一个
main
函数,该函数创建名为data
的对象,其属性a
设置为 1。 main
使用Promise.all
同时执行两个函数:fn1(data)
和fn2(data)
。这两个函数都接收同一个名为data
的对象。fn1
检查data.a === 1
。最初,这个条件为真。- 在 if 块内部,
fn1
调用await fn0()
。这一点很重要,因为它允许其他代码在fn1
继续执行之前先行运行。 - 在
fn1
暂停期间,fn2
执行。它将data.a
改为 2。 fn1
在fn0
执行完成后恢复执行。它再次检查data.a === 1
,但现在这个条件为假,因为fn2
修改了data
。- 控制台输出显示这种变化:在调用
await fn0()
之前条件为真,之后则为假。
const fn0 = async () => {
console.log("执行fn0");
};
const fn1 = async (data) => {
console.log("执行fn1");
if (data.a === 1) {
console.log("条件", data.a === 1);
await fn0();
console.log("条件不变", data.a === 1);
console.log("结束if语句块");
}
};
const fn2 = (data) => {
console.log("执行fn2");
data.a = 2;
};
async function main() {
const data = {
a: 1,
};
await Promise.all([fn1(data), fn2(data)]);
}
await main();
从日志的输出可以看出,f2
函数在执行 f1
的时候被执行了。
正在执行 fn1
条件为真
正在执行 fn0
正在执行 fn2
条件为假
if 语句执行完毕
简化后的事件列表如下:
- 初始状态:当调用
Promise.all
时,fn1
和fn2
被添加到事件队列中。图中显示它们在各自函数的第 1 行开始执行。 - 第一次执行:事件循环首先拾取
fn1
并执行,直到遇到await fn0()
调用为止。 - 回调添加:此时,一个回调被添加到队列中,以便在第 5 行恢复 fn1 的执行(在 await 之后)。同时,
fn0
开始执行。 - 队列中的下一个:在
fn0
执行完毕后,事件循环接着处理队列中的下一个项目,即执行完fn2
。 - 最终步骤:队列中唯一剩下的项目是恢复 fn1 的回调,该回调将在第 5 行继续 fn1 的执行。事件循环拾取此回调,
fn1
从该点继续执行。
这个例子展示了异步代码如何导致竞态状况。在没有实际异步 I/O 操作的情况下,对象的状态在执行 fn1
的过程中意外地发生了变化。这种行为是因为使用了 async/await
关键字,它们通过与 Node.js 的事件循环交互,提供了并发执行的可能性。
事件循环是一个复杂且充满细节的概念。这篇帖子主要探讨了函数调度的基本概念以及它与 Node.js 中 async/await 的关系,但也总是有更多的东西可以学习。如果你对深入了解感兴趣,我推荐下面这些资源:
我们还看到了如何使用异步函数来控制事件循环的运行,如果不正确理解,不仅强大而且危险的。
在接下来的文章中,我们将深入了解 Node.js 的垃圾收集器,这也是成为顶尖的 Node.js 工程师(仅占 1%)的关键方面。敬请关注!
共同學習,寫下你的評論
評論加載中...
作者其他優質文章