【翻译】深入JavaScript事件循环:运行时机制揭秘

30 阅读3分钟

原文链接:certificates.dev/blog/inside…

深入理解 JavaScript 事件循环。学习调用堆栈、微任务和宏任务如何协同工作,高效处理异步代码,确保应用程序快速、可预测且响应灵敏。

作者:Martin Ferret

1. 单车道道路类比

JavaScript 在单线程环境中运行。你可以将其想象成一条单行道,每次只能有一辆车——即一段代码——通行。

这辆车代表着调用堆栈,即当前正在执行的函数所在的位置。

当车辆需要加油时(例如函数触发 fetch()setTimeout() 等异步操作),它不会阻塞整条道路,而是简单地靠边停靠。其他车辆仍可继续通行。

加完油后,车辆将通过两个队列之一重新加入车流:微任务队列(用于Promise、queueMicrotaskMutationObserver )或宏任务队列(用于 setTimeoutsetInterval 或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 将不再显得随机。它变得可预测、透明且完全可控。