JavaScript的秘密生活:异步

发布: (2025年12月21日 GMT+8 12:01)
4 min read
原文: Dev.to

Source: Dev.to

Introduction

Timothy 叹了口气,把额头靠在绘图桌的凉爽橡木上。逻辑图纸摊开在他面前。

“我卡住了,Margaret。我在写‘图书检索’序列的指令。每次代码让引擎去获取大量数据时,整个界面就会卡死。它就在那儿,什么也不做,只是等数据到达。这对用户来说太不礼貌了。”

Margaret 安慰地笑了笑并回答:

“这并不不礼貌,Timothy。它只是 同步 的。要解决这个问题,你必须了解我们所处环境的具体架构。它从 调用栈(Call Stack) 开始。”

Call Stack

  • JavaScript 引擎是 单线程 的,这意味着它只有一个调用栈,并且一次只能执行一行代码。
  • 如果一个慢函数(例如网络请求或大量计算)被放入调用栈,引擎在该函数完成之前无法做其他事情。这会阻塞 UI,导致卡死。

Web APIs

JavaScript 并不在调用栈上完成这些工作,而是将其交给 Web API(计时器、网络等)在主线程之外运行。

  • 当你调用 setTimeoutfetch 时,任务会交给这些后台服务。
  • 浏览器在后台处理等待或下载,而调用栈继续执行下一行代码。

Task Queues

当 Web API 完成工作后,它不能随意把代码推回调用栈。它必须把回调放入以下两种队列之一:

宏任务队列(Macrotask Queue,Callback Queue)

  • 示例: setTimeoutsetInterval、I/O 操作
  • 作用: 保存标准的异步任务。

微任务队列(Microtask Queue)

  • 示例: Promise.thenqueueMicrotaskawait
  • 作用: 更高优先级的队列;引擎将其视为紧急任务。

Event Loop

事件循环(Event Loop) 持续监视调用栈和队列:

  1. 检查调用栈: 若不为空,则等待。
  2. 运行微任务: 若栈为空,运行微任务队列中的所有任务直至为空。
  3. 运行宏任务: 在所有微任务清空后,运行宏任务队列中的一个项目。

Example: Priority Order

Timothy 编写了一个测试用例来验证优先级顺序:

console.log("Start");

setTimeout(function() {
    console.log("Timeout");
}, 0);

Promise.resolve().then(function() {
    console.log("Promise");
});

console.log("End");

Execution Walkthrough

  1. console.log("Start") – 同步的,立即在调用栈上执行。
  2. setTimeout(..., 0) – 交给 Web API,立即完成,其回调进入 宏任务队列
  3. Promise.resolve().then(...) – 交给 Web API,立即解析,其回调进入 微任务队列
  4. console.log("End") – 同步的,立即执行。

同步代码执行完毕后,调用栈为空。此时事件循环开始处理队列:

  • 微任务队列 中有 Promise 的回调 → 首先运行。
  • 宏任务队列 中有 setTimeout 的回调 → 在微任务全部清除后运行。

Console Output

Start
End
Promise
Timeout

即使延迟为零秒,setTimeout 也总会输给 Promise,因为宏任务必须等微任务完成后才能执行。

Back to Blog

相关文章

阅读更多 »

裸机前端

Bare-metal frontend 介绍 现代前端应用已经变得非常丰富、复杂且精细。它们不再只是简单的 UI 轮询数据。它们……