JavaScript的秘密生活:异步
Source: Dev.to
Introduction
Timothy 叹了口气,把额头靠在绘图桌的凉爽橡木上。逻辑图纸摊开在他面前。
“我卡住了,Margaret。我在写‘图书检索’序列的指令。每次代码让引擎去获取大量数据时,整个界面就会卡死。它就在那儿,什么也不做,只是等数据到达。这对用户来说太不礼貌了。”
Margaret 安慰地笑了笑并回答:
“这并不不礼貌,Timothy。它只是 同步 的。要解决这个问题,你必须了解我们所处环境的具体架构。它从 调用栈(Call Stack) 开始。”
Call Stack
- JavaScript 引擎是 单线程 的,这意味着它只有一个调用栈,并且一次只能执行一行代码。
- 如果一个慢函数(例如网络请求或大量计算)被放入调用栈,引擎在该函数完成之前无法做其他事情。这会阻塞 UI,导致卡死。
Web APIs
JavaScript 并不在调用栈上完成这些工作,而是将其交给 Web API(计时器、网络等)在主线程之外运行。
- 当你调用
setTimeout或fetch时,任务会交给这些后台服务。 - 浏览器在后台处理等待或下载,而调用栈继续执行下一行代码。
Task Queues
当 Web API 完成工作后,它不能随意把代码推回调用栈。它必须把回调放入以下两种队列之一:
宏任务队列(Macrotask Queue,Callback Queue)
- 示例:
setTimeout、setInterval、I/O 操作 - 作用: 保存标准的异步任务。
微任务队列(Microtask Queue)
- 示例:
Promise.then、queueMicrotask、await - 作用: 更高优先级的队列;引擎将其视为紧急任务。
Event Loop
事件循环(Event Loop) 持续监视调用栈和队列:
- 检查调用栈: 若不为空,则等待。
- 运行微任务: 若栈为空,运行微任务队列中的所有任务直至为空。
- 运行宏任务: 在所有微任务清空后,运行宏任务队列中的一个项目。
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
console.log("Start")– 同步的,立即在调用栈上执行。setTimeout(..., 0)– 交给 Web API,立即完成,其回调进入 宏任务队列。Promise.resolve().then(...)– 交给 Web API,立即解析,其回调进入 微任务队列。console.log("End")– 同步的,立即执行。
同步代码执行完毕后,调用栈为空。此时事件循环开始处理队列:
- 微任务队列 中有 Promise 的回调 → 首先运行。
- 宏任务队列 中有
setTimeout的回调 → 在微任务全部清除后运行。
Console Output
Start
End
Promise
Timeout
即使延迟为零秒,setTimeout 也总会输给 Promise,因为宏任务必须等微任务完成后才能执行。