🚀 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


🔄 事件循环

什么是事件循环?

事件循环是一个调度器。

它的职责是持续监控调用栈。

每当调用栈为空时,它会检查:

  1. 微任务队列
  2. 回调队列

事件循环首先执行所有微任务

只有在微任务队列为空后,它才会执行一个宏任务。


事件循环工作流程

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 APINode API 处理异步操作。
  • 已完成的异步回调会被放入队列中。
  • 微任务队列的优先级高于回调队列
  • 事件循环持续检查调用栈并调度任务。
  • Promise 总是先于 setTimeout() 回调执行,因为微任务优先被处理。
  • JavaScript 通过事件循环实现并发,而真正的并行需要 Web Workers 或 Worker Threads。

🎯 结论

理解事件循环和 JavaScript 的并发模型对于编写高效的异步代码至关重要。调用栈、Web API、任务队列和事件循环等概念解释了为什么 JavaScript 即使在处理定时器、网络请求和用户交互时也能保持响应。

一旦你理解了这些基础知识,像 async/await、API 调用以及 React 和 Node.js 等现代框架等主题就会变得更容易学习,因为它们都建立在相同的异步执行模型之上。