JavaScript 的事件循环(Event Loop)是其异步编程的核心机制,它决定了代码的执行顺序。以下是事件循环的核心原理和工作流程:
一、核心组成
- 调用栈(Call Stack)
用于执行同步代码的栈结构(后进先出)。
遇到函数调用时压入栈顶,执行完毕后弹出。
- 任务队列(Task Queues)
-
宏任务队列(MacroTask Queue)
-
setTimeout
,setInterval
, DOM 事件回调, I/O 操作等。
-
-
微任务队列(MicroTask Queue)
-
Promise.then/catch/finally
,queueMicrotask
,MutationObserver
。
-
- 事件循环线程(Event Loop Thread)
- 持续检查调用栈是否为空,调度队列中的任务。
二、事件循环工作流程
- 执行同步代码
- 同步代码直接进入调用栈,逐行执行。
- 处理异步任务
-
遇到异步操作(如
setTimeout
)时:浏览器将回调注册到 Web API 环境,等待触发条件(如定时器到期)。
条件满足后,回调被推入对应的任务队列。
- 事件循环调度
-
当调用栈为空时:
优先清空微任务队列(全部执行完毕)。
执行一个宏任务。
再次清空微任务队列。
重复此过程。
三、执行顺序示例
console.log("1"); // 同步代码
setTimeout(() => console.log("2"), 0); // 宏任务
Promise.resolve().then(() => {
console.log("3"); // 微任务
setTimeout(() => console.log("4"), 0); // 嵌套宏任务
});
console.log("5"); // 同步代码
输出顺序:1 → 5 → 3 → 2 → 4
-
解析:
同步代码输出
1
和5
。清空微任务队列,输出
3
,同时将setTimeout(4)
加入宏任务队列。执行宏任务队列,先输出
2
,再执行下一个宏任务输出4
。
四、关键特性
- 微任务优先级高于宏任务
- 每次调用栈清空后,必须执行完所有微任务才会处理宏任务。
- 渲染时机
- 在微任务执行完毕后、下一个宏任务执行前,浏览器可能执行渲染(如
requestAnimationFrame
)。
- Node.js 差异
- Node.js 的事件循环分为多个阶段(如
timers
、poll
、check
),微任务在阶段切换间处理。
五、常见问题
- 为什么
Promise.then
比setTimeout
先执行?
- 微任务队列优先级高,会在当前宏任务结束后立即执行。
- 如何避免阻塞事件循环?
- 避免长时间同步代码(如复杂计算),拆分任务或用
Web Worker
。
- 死循环风险
- 微任务中递归添加微任务会导致事件循环永远无法处理宏任务。
六、调试技巧
使用浏览器开发者工具的 Sources 面板 查看调用栈和任务队列。
使用
console.trace()
追踪函数调用路径。
Web Worker
Web Worker 是浏览器提供的一种技术,允许 JavaScript 在后台线程中运行代码,从而避免阻塞主线程(UI 线程),实现多线程并行处理。它是解决 JavaScript 单线程限制的核心方案之一,尤其适合处理计算密集型任务或耗时操作。
一、核心特性
-
独立线程
- Web Worker 运行在独立的全局上下文中,与主线程隔离。
- 无法直接操作 DOM(避免线程冲突)。
-
通过消息机制与主线程通信(
postMessage
和onmessage
)。
-
线程类型
-
专用 Worker(Dedicated Worker)
- 仅能被创建它的脚本使用,生命周期与创建者绑定。
-
共享 Worker(Shared Worker)
- 可被多个脚本共享(需同源),通过端口(
port
)通信。
- 可被多个脚本共享(需同源),通过端口(
-
Service Worker
- 用于离线缓存、网络代理等(属于 PWA 核心技术)。
-
专用 Worker(Dedicated Worker)
-
浏览器支持
- 现代浏览器全面支持(包括移动端),IE 10+ 部分支持。
二、使用场景
-
计算密集型任务
- 图像/视频处理(如 Canvas 像素计算)。
- 大数据分析(如排序、加密解密)。
- 复杂数学运算(如物理模拟、3D 渲染)。
-
避免 UI 阻塞
- 文件读写(如大文件解析)。
- 长时间轮询或网络请求。
-
后台任务
- 日志上报、数据预加载、定时任务。
三、基本用法
1. 创建专用 Worker
// 主线程代码(main.js)
const worker = new Worker('worker.js');
// 向 Worker 发送数据
worker.postMessage({ type: 'calc', data: 1000 });
// 接收 Worker 返回的数据
worker.onmessage = (e) => {
console.log('Result:', e.data);
};
2. Worker 线程代码(worker.js)
// 监听主线程消息
self.onmessage = (e) => {
if (e.data.type === 'calc') {
const result = heavyCalculation(e.data.data);
self.postMessage(result); // 返回结果
}
};
function heavyCalculation(n) {
// 模拟耗时计算(如斐波那契数列)
let sum = 0;
for (let i = 0; i < n; i++) sum += i;
return sum;
}
四、通信机制
-
数据传输方式
- 结构化克隆算法:自动深拷贝对象(支持大多数数据类型,如 ArrayBuffer)。
-
Transferable Objects:零拷贝转移所有权(如
postMessage(buffer, [buffer])
)。
-
错误处理
- 通过
worker.onerror
捕获 Worker 内部错误。
- 通过
五、限制与注意事项
-
无法访问的 API
- DOM、
window
、document
、parent
对象。 -
alert()
、confirm()
等 UI 相关方法。
- DOM、
-
作用域隔离
- Worker 内部使用
self
代替全局对象(而非window
)。
- Worker 内部使用
-
资源限制
- 浏览器可能限制同时创建的 Worker 数量。
- 大量 Worker 会占用内存和 CPU。
六、优化技巧
-
复用 Worker
- 避免频繁创建/销毁,通过消息复用实例。
-
任务拆分
- 将大任务拆分为子任务分批次处理。
-
终止 Worker
- 使用
worker.terminate()
及时释放资源。
- 使用
七、替代方案
-
WebAssembly
- 处理更复杂的计算,性能接近原生代码。
-
分时函数(setTimeout 分块)
- 将任务拆分为小块通过事件循环穿插执行(兼容性更好,但效率较低)。
总结
- 适用场景:需要并行处理、避免主线程阻塞的任务。
- 核心优势:提升页面响应速度,优化用户体验。
- 典型应用:在线编辑器、数据可视化、游戏引擎等。