🚀 JavaScript 事件循环与并发模型:新手指南
"JavaScript 是单线程的,但它可以处理定时器、API 请求、用户交互和异步任务而不会冻结应用程序。这是如何实现的?答案在于事件循环和 JavaScript 的并发模型。"
当我第一次开始学习 JavaScript 时,有一个问题总是困扰着我:
如果 JavaScript 一次只能执行一个任务,它是如何同时执行多个任务(如 API 调用、定时器和用户交互)的?
答案是JavaScript 的事件循环和并发模型。
在本文中,我们将通过简单的解释、图表和实际示例来理解 JavaScript 如何在后台管理异步操作。
📚 你将学到什么
- 什么是 JavaScript 的并发模型?
- 为什么 JavaScript 是单线程的
- 调用栈
- Web API / Node API
- 回调队列(宏任务队列)
- 微任务队列
- 事件循环
-
setTimeout()与 Promise 的对比 - 协作式并发
- 并发与并行
让我们开始吧!
🧵 JavaScript 是单线程的
JavaScript 使用单线程执行代码,意味着它一次只能执行一个操作。
想象一位厨师独自在厨房里工作。
如果厨师开始烹饪一道菜,在完成之前无法烹饪另一道菜。
同样,JavaScript 一次执行一条语句。
示例:
console.log("任务 1");
console.log("任务 2");
console.log("任务 3");
Enter fullscreen mode Exit fullscreen mode
输出
任务 1
任务 2
任务 3
Enter fullscreen mode Exit fullscreen mode
一切都按顺序执行。
🤔 那么 JavaScript 是如何处理多个任务的?
现代应用程序需要:
- 从服务器获取数据
- 等待定时器
- 读取文件
- 处理按钮点击
- 播放视频
如果 JavaScript 等待每个操作完成后再继续,网页就会冻结。
相反,JavaScript 使用了并发模型。
⚙️ 什么是并发模型?
并发模型允许 JavaScript 高效地管理多个任务,而无需在主线程上同时执行它们。
JavaScript 将耗时的操作委托给浏览器(Web API)或 Node.js(Node API)。当这些操作在 JavaScript 引擎外部运行时,主线程继续执行其他代码。
当异步任务完成时,其回调会被排队并由事件循环稍后执行。
🧱 调用栈
什么是调用栈?
调用栈是一个跟踪函数执行的数据结构。
它遵循LIFO(后进先出)原则。
每当调用一个函数时:
- 它会被推入栈中。
- 当它完成时,它会被弹出。
示例
function first() {
console.log("First");
}
function second() {
first();
console.log("Second");
}
second();
Enter fullscreen mode Exit fullscreen mode
输出
First
Second
Enter fullscreen mode Exit fullscreen mode
执行过程
调用栈
↓
second()
↓
first()
↓
console.log()
↓
first() 被移除
↓
console.log()
↓
second() 被移除
Enter fullscreen mode Exit fullscreen mode
🌐 Web API / Node API
JavaScript 本身无法执行定时器或网络请求等操作。
这些功能由运行环境提供。
浏览器 Web API
- setTimeout()
- setInterval()
- fetch()
- DOM 事件
- localStorage
- Geolocation
Node.js API
- 文件系统 (fs)
- HTTP 服务器
- 流
- 定时器
- 进程 API
这些 API 在 JavaScript 继续执行其他代码的同时执行异步工作。
示例
console.log("Start");
setTimeout(() => {
console.log("Timer Completed");
}, 2000);
console.log("End");
Enter fullscreen mode Exit fullscreen mode
输出
Start
End
Timer Completed
Enter fullscreen mode Exit fullscreen mode
请注意,JavaScript 不会等待两秒钟。
📥 回调队列(宏任务队列)
一旦异步操作完成,其回调就会被放入回调队列(也称为宏任务队列)中。
宏任务示例
- setTimeout()
- setInterval()
- DOM 事件
- setImmediate() (Node.js)
示例
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
Enter fullscreen mode Exit fullscreen mode
输出
Start
End
Timeout
Enter fullscreen mode Exit fullscreen mode
即使定时器延迟为0 毫秒,回调也只有在调用栈为空后才会执行。
⚡ 微任务队列
微任务队列存储优先级更高的异步任务。
示例包括:
- Promise.then()
- Promise.catch()
- Promise.finally()
- queueMicrotask()
微任务总是先于宏任务执行。
示例
console.log("Start");
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
Enter fullscreen mode Exit fullscreen mode
输出
Start
End
Promise
Enter fullscreen mode Exit fullscreen mode
🔄 事件循环
什么是事件循环?
事件循环是一个调度器。
它的职责是持续监控调用栈。
每当调用栈为空时,它会检查:
- 微任务队列
- 回调队列
事件循环首先执行所有微任务。
只有在微任务队列为空后,它才会执行一个宏任务。
事件循环工作流程
JavaScript 代码
│
▼
调用栈
│
▼
异步操作
│
▼
Web API / Node API
│
▼
┌──────────────────┐
│ 微任务队列 │
│ Promise.then() │
│ queueMicrotask() │
└──────────────────┘
┌──────────────────┐
│ 回调队列 │
│ setTimeout() │
│ DOM 事件 │
└──────────────────┘
│
▼
事件循环
│
▼
调用栈
Enter fullscreen mode Exit fullscreen mode
🆚 setTimeout() 与 Promise 的对比
最常见的面试问题之一是 setTimeout() 和 Promise 的执行顺序。
示例
console.log("Start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("Promise");
});
console.log("End");
Enter fullscreen mode Exit fullscreen mode
输出
Start
End
Promise
setTimeout
Enter fullscreen mode Exit fullscreen mode
为什么 Promise 先执行?
让我们一步步理解执行过程。
步骤 1
console.log("Start");
Enter fullscreen mode Exit fullscreen mode
输出
Start
Enter fullscreen mode Exit fullscreen mode
步骤 2
setTimeout(...)
Enter fullscreen mode Exit fullscreen mode
定时器已在 Web API 中注册。
步骤 3
Promise.resolve().then(...)
Enter fullscreen mode Exit fullscreen mode
回调已放入微任务队列中。
步骤 4
console.log("End");
Enter fullscreen mode Exit fullscreen mode
输出
End
Enter fullscreen mode Exit fullscreen mode
步骤 5
调用栈为空。
事件循环首先检查微任务队列。
输出
Promise
Enter fullscreen mode Exit fullscreen mode
步骤 6
所有微任务完成后,事件循环执行回调队列。
输出
setTimeout
Enter fullscreen mode Exit fullscreen mode
🤝 协作式并发
JavaScript 使用协作式并发。
这意味着:
- 一次只能执行一段 JavaScript 代码。
- 任务在完成后自愿让出控制权。
- 没有任务会中断另一个任务。
示例
console.log("Task 1");
setTimeout(() => {
console.log("Task 2");
}, 0);
console.log("Task 3");
Enter fullscreen mode Exit fullscreen mode
输出
Task 1
Task 3
Task 2
Enter fullscreen mode Exit fullscreen mode
⚡ 并发与并行
许多初学者认为这些术语含义相同,但它们是不同的。
并发
通过在多个任务之间高效切换来管理多个任务。
示例:
一位厨师在多道菜之间切换来准备多道菜。
并行
使用多个工作线程或 CPU 核心同时执行多个任务。
示例:
三位厨师同时烹饪三道不同的菜。
JavaScript 与并行
JavaScript 的主线程是非并行的。
但是,可以使用以下方式实现真正的并行:
- Web Workers(浏览器)
- Worker Threads(Node.js)
这些为 CPU 密集型工作创建了单独的线程。
📊 微任务队列与回调队列的对比
| 特性 | 微任务队列 | 回调队列 |
|---|---|---|
| 优先级 | 更高 | 更低 |
| 示例 | Promise.then()、catch()、finally()、queueMicrotask() | setTimeout()、setInterval()、DOM 事件 |
| 执行 | 在调用栈为空后立即运行 | 在所有微任务完成后运行 |
📝 关键要点
- JavaScript 是单线程的。
- 调用栈执行同步代码。
- Web API 和 Node API 处理异步操作。
- 已完成的异步回调会被放入队列中。
- 微任务队列的优先级高于回调队列。
- 事件循环持续检查调用栈并调度任务。
- Promise 总是先于
setTimeout()回调执行,因为微任务优先被处理。 - JavaScript 通过事件循环实现并发,而真正的并行需要 Web Workers 或 Worker Threads。
🎯 结论
理解事件循环和 JavaScript 的并发模型对于编写高效的异步代码至关重要。调用栈、Web API、任务队列和事件循环等概念解释了为什么 JavaScript 即使在处理定时器、网络请求和用户交互时也能保持响应。
一旦你理解了这些基础知识,像 async/await、API 调用以及 React 和 Node.js 等现代框架等主题就会变得更容易学习,因为它们都建立在相同的异步执行模型之上。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.