介绍
JavaScript 主要在 Node.js 和浏览器中的单个线程上执行(有一些例外,例如工作线程,这超 出了当前文章的范围)。在本文中,我将尝试解释 Node.js 的并发机制,即事件循环。
例子
我相信通过示例学习是最好的,因此我将从 4 个简单的代码示例开始。我将分析示例,然后深入研究 Node.js 的架构。
Example 1:
console.log(1);
console.log(2);
console.log(3);
// Output:
// 1
// 2
// 3
这个例子很简单,
第一步 console.log(1) 进入调用栈,被执行然后删除,
第二步 console.log(2) 进入调用栈,被执行然后删除,以此类推 console.log(3)。
示例 1 过程的可视化
Example 2:
console.log(1);
setTimeout(function foo(){
console.log(2);
}, 0);
console.log(3);
// Output:
// 1
// 3
// 2
我们可以在这个例子中看到我们立即运行 setTimeout,所以我们希望 console.log(2) 在 console.log(3) 之前,但事实并非如此,让我们了解它背后的机制。
基本事件循环架构(我们稍后会深入探讨)
- 网络API:它们内置于您的网络浏览器中,能够从浏览器和周围的计算机环境中公开数据,并用它来做有用的复杂事情。它们不是 JavaScript 语言本身的一部分,而是构建在核心 JavaScript 语言之上,为您提供在 JavaScript 代码中使用的额外超能力。例如,Geolocation API 提供了一些简单的 JavaScript 结构来检索位置数据,因此您可以说,在 Google 地图上绘制您的位置。在后台,浏览器实际上使用一些复杂的低级代码(例如 C++)与设备的 GPS 硬件(或任何可用于确定位置数据的东西)通信,检索位置数据,并将其返回给浏览器环境以供使用在你的代码中。但同样,这种复杂性已通过 API 从您那里抽象出来。
- Event Loop & Callback Queue:完成Web Apis执行的函数被移动到Callback Queue,这是一个常规的队列数据结构,Event Loop负责从Callback Queue中取出下一个函数并将函数发送到执行函数的调用堆栈
执行顺序
- 当前在调用堆栈中的所有函数都会被执行,然后它们会从调用堆栈中弹出。
- 当调用栈为空时,所有排队的任务被一个一个地弹出到调用栈并执行,然后它们被弹出调用栈
让我们理解示例2
- console.log(1) 方法被调用并放置在调用堆栈上并被执行。
2. setTimeout 方法被调用并放置在调用堆栈上并被执行,此执行创建一个对 setTimeout Web Api 的新调用 0 毫秒,当它完成时(立即,或者更准确地说,它会更好“尽快”),Web Api 将调用移至回调队列。
3. console.log(3) 方法被调用并放置在调用栈中并被执行。
4. Event Loop 发现 Call Stack 为空,从 Callback Queue 中取出“foo”方法并将其放入 Call Stack,然后执行 console.log(2)。
示例 2 过程的可视化
setTimeout(function, delay)中的delay参数并不代表函数执行后的精确延时时间。它代表等待函数执行的最短时间。
Example 3:
console.log(1);
setTimeout(function foo() {
console.log(‘foo’);
}, 3500);
setTimeout(function boo() {
console.log(‘boo’);
}, 1000);
console.log(2);
// Output:
// 1
// 2
// 'boo'
// 'foo'
示例 3过程的可视化
Example 4:
console.log(1);
setTimeout(function foo() {
console.log(‘foo’);
}, 6500);
setTimeout(function boo() {
console.log(‘boo’);
}, 2500);
setTimeout(function baz() {
console.log(‘baz’);
}, 0);
for (const value of [‘A’, ‘B’]) {
console.log(value);
}
function two() {
console.log(2);
}
two();
// Output:
// 1
// 'A'
// 'B'
// 2
// 'baz'
// 'boo'
// 'foo'
示例 4 过程的可视化
事件循环继续执行任务队列中等待的所有回调。在任务队列中,任务大致分为两类,即微任务Micro-Task 和宏任务macro-tasks。
宏任务(任务队列)和微任务
更准确地说,实际上有两种类型的队列。
1.宏任务队列(或简称任务队列)。
2.微任务队列。
- 常见的宏任务是 setTimeout、setInterval 和 setImmediate。
- 常见的 Micro-Task 有 process.nextTick 和 Promise 回调。
执行顺序
- 当前在调用堆栈中的所有函数都会被执行,然后它们会从调用堆栈中弹出。
- 当调用栈为空时,所有排队的微任务被一个一个弹出到调用栈并执行,然后从调用栈中弹出。
- 当调用栈和微任务队列都为空时,所有排队的宏任务被一个一个弹出到调用栈并执行,然后被弹出调用栈。
Example 5:
console.log(1);
setTimeout(function foo() {
console.log(‘foo’);
}, 0);
Promise.resolve()
.then(function boo() {
console.log(‘boo’);
});
console.log(2);
// Output:
// 1
// 2
// 'boo'
// 'foo'
- console.log(1) 方法被调用并放置在调用堆栈上并被执行。
- 正在执行 SetTimeout,将 console.log('foo') 移至 SetTimeout Web Api,0 毫秒后移至Macro -Task Queue。
- Promise.resolve() 被调用,它被解析,然后 .then() 方法被移动到Micro -Task 队列。
- console.log(2) 方法被调用并放置在调用堆栈上并被执行。
- Event Loop 看到调用栈是空的,它首先从 Micro-Task 队列中取出任务,即 Promise 任务,将 console.log('boo') 放在调用栈上并执行它。
- 事件循环看到调用栈为空,然后它看到微任务为空,然后它从宏任务队列中取出下一个任务,即 SetTimeout 任务,将 console.log('foo')在调用堆栈上并执行它。
![](https://img-blog.csdnimg.cn/41344fc8f2c148c7942aa1ad4c76d930.gif)
示例 5 过程的可视化