原文链接:certificates.dev/blog/inside…
深入理解 JavaScript 事件循环。学习调用堆栈、微任务和宏任务如何协同工作,高效处理异步代码,确保应用程序快速、可预测且响应灵敏。
1. 单车道道路类比
JavaScript 在单线程环境中运行。你可以将其想象成一条单行道,每次只能有一辆车——即一段代码——通行。
这辆车代表着调用堆栈,即当前正在执行的函数所在的位置。
当车辆需要加油时(例如函数触发 fetch() 或 setTimeout() 等异步操作),它不会阻塞整条道路,而是简单地靠边停靠。其他车辆仍可继续通行。
加完油后,车辆将通过两个队列之一重新加入车流:微任务队列(用于Promise、queueMicrotask 或 MutationObserver )或宏任务队列(用于 setTimeout 、 setInterval 或DOM事件)。
事件循环充当交通管制员。
事件巡航持续检测道路是否畅通。当道路空闲时,便从队列中提取下一辆待行车辆放行,始终优先处理微任务而非宏任务。
正是这个机制使JavaScript呈现异步特性——尽管它从不会同时执行多个任务。
2. 核心组件
JavaScript 运行时围绕三个核心结构构建。
调用栈
这是同步代码的执行区域。每次函数调用都会被压入栈中,完成后从栈中弹出。
堆
用于存储对象、闭包和引用的庞大内存区域。
队列
用于存放异步任务,待调用栈清空后执行。
事件循环负责监控所有这些组件。它确保任务按正确顺序处理,主线程永不空闲,异步操作永不阻塞执行。
3. 微任务与宏任务
要理解异步代码的执行机制,需要区分微任务和宏任务。
微任务(如Promises和queueMicrotask)会在当前栈清空后立即执行,且优先于下一个渲染周期。它们具有更高优先级,总会在任何宏任务开始前运行。
宏任务(如定时器、事件或I/O操作)则在所有微任务完成后执行。每个宏任务代表事件循环的新"时钟周期"。
示例:
console.log('Start');
setTimeout(() => console.log('Macrotask: Timeout'), 0);
Promise.resolve().then(() => console.log('Microtask: Promise'));
console.log('End');
输出:
Start
End
Microtask: Promise
Macrotask: Timeout
Promise回调在超时之前执行,因为微任务总是优先处理。
4. 实践案例:数据获取与界面更新
考虑一种常见的用户界面模式。当用户点击按钮时,先显示加载提示,然后从API获取数据。
button.addEventListener('click', () => {
list.textContent = 'Loading...';
fetch('/api/items')
.then(res => res.json())
.then(data => {
list.textContent = data.join(', ');
});
});
文本“Loading…”会立即显示,因为设置该文本是同步操作。
fetch() 调用以异步方式运行,当数据准备就绪时,promise 便会解析并更新 DOM。
事件循环在协调这些操作时不会冻结界面或阻塞用户交互。
5. 事件循环为何重要
理解事件循环对编写高效的JavaScript至关重要。
性能 它能帮助你避免阻塞操作,从而防止渲染或用户输入变慢。
可预测性 它让你能够推断任务顺序,避免隐蔽的异步时序错误。
框架 所有现代前端或后端框架(如React、Angular或Node.js)都依赖事件循环的调度模型。
控制
理解微任务与宏任务的行为机制后,你就能主动掌控异步代码的执行时机与方式。
事件循环是 JavaScript 运行时的核心脉搏。它以步进式方式管理所有操作,从同步调用到异步操作无一遗漏。
当你掌握它在栈、微任务和宏任务间切换的机制后,异步 JavaScript 将不再显得随机。它变得可预测、透明且完全可控。