在现代 Web 开发中,理解异步编程的本质和运行机制对于编写高性能应用至关重要。本文将探讨事件循环机制、主线程与 Web Worker。
异步的本质
异步编程的核心在于解耦任务的发起和执行。当我们发起一个异步操作时,我们不是在等待它完成,而是告诉系统:"当这个任务完成时,请通知我"。异步的本质特征
- 非阻塞(Non-blocking):发起异步操作后,主线程继续执行后续代码,不会等待
- 回调通知(Callback Notification):异步操作完成后,通过回调函数通知调用者
- 时间解耦(Temporal Decoupling):任务的发起时间和执行时间是分离的
并发 vs 并行
理解异步的本质,还需要区分两个重要概念:
-
并发(Concurrency):多个任务在同一时间段内交替执行,看起来像是同时进行
- JavaScript 的事件循环就是并发模型
- 通过快速切换任务,给人一种"同时执行"的错觉
-
并行(Parallelism):多个任务真正同时在不同的物理核心上执行
- Web Worker 提供了真正的并行执行能力
- 需要多核 CPU 支持
javascript
// 并发示例(主线程)
async function concurrent() {
console.log('任务1开始');
await sleep(1000);
console.log('任务1结束');
console.log('任务2开始');
await sleep(1000);
console.log('任务2结束');
}
// 总耗时:2秒(任务交替执行)
// 并行示例(Web Worker)
function parallel() {
const worker1 = new Worker('task1.js');
const worker2 = new Worker('task2.js');
worker1.postMessage('start'); // 在独立线程1执行
worker2.postMessage('start'); // 在独立线程2执行
}
// 总耗时:1秒(任务同时执行)
事件循环与任务队列
事件循环(Event Loop)是 JavaScript 异步编程的核心机制,它协调了同步代码、异步任务、浏览器渲染之间的执行顺序。
事件循环的基本结构
JavaScript 运行时包含以下几个关键组件:
javascript
┌─────────────────────────────────────┐
│ JavaScript 运行时 │
│ │
│ ┌──────────────┐ │
│ │ 调用栈 │ ← 执行同步代码 │
│ │ (Call Stack)│ │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ 微任务队列 │ ← Promise.then │
│ │ (Micro Queue)│ async/await │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ │
│ │ 宏任务队列 │ ← setTimeout │
│ │ (Macro Queue)│ setInterval │
│ └──────────────┘ I/O 操作 │
│ │
└─────────────────────────────────────┘
事件循环的执行流程
事件循环的完整执行顺序可以概括为:
markdown
1. 执行调用栈中的所有同步代码
└─ 直到调用栈清空
2. 执行所有微任务
└─ 如果微任务产生新的微任务,继续执行
└─ 直到微任务队列完全清空
3. 执行一个宏任务
└─ 从宏任务队列中取出最早的一个任务
4. 回到步骤 2(如果有渲染需求,则先执行渲染)
让我们通过代码示例理解这个流程:
javascript
console.log('1 - 同步代码开始');
setTimeout(() => {
console.log('2 - setTimeout 1');
Promise.resolve().then(() => {
console.log('3 - Promise in setTimeout');
});
}, 0);
Promise.resolve().then(() => {
console.log('4 - Promise 1');
setTimeout(() => {
console.log('5 - setTimeout in Promise');
}, 0);
}).then(() => {
console.log('6 - Promise 2');
});
setTimeout(() => {
console.log('7 - setTimeout 2');
}, 0);
console.log('8 - 同步代码结束');
// 输出顺序:
// 1 - 同步代码开始
// 8 - 同步代码结束
// 4 - Promise 1
// 6 - Promise 2
// 2 - setTimeout 1
// 3 - Promise in setTimeout
// 7 - setTimeout 2
// 5 - setTimeout in Promise
常见的任务队列
常见的宏任务,每次事件循环只执行一个宏任务。
setTimeout
/setInterval
- 定时器回调- I/O 操作 - 文件读写、网络请求(XMLHttpRequest)
- UI 事件 - 用户交互(click、scroll 等)
MessageChannel
/postMessage
常见的微任务,每次事件循环会清空整个微任务队列。
Promise.then
/catch
/finally
async/await
(本质是 Promise)MutationObserver
- DOM 变化监听queueMicrotask
- 显式添加微任务
主线程 vs Web Worker
理解主线程和 Web Worker 的区别,是掌握 Web 异步编程的重要一环。这两者的异步模型有着本质的不同。
主线程:并发执行模型
主线程通过事件循环实现并发,但所有代码都在同一个线程上执行。
主线程的特点
javascript
// 主线程的单线程特性
console.time('主线程执行');
// 即使使用 Promise,也是在同一个线程上执行
Promise.resolve().then(() => {
// 密集计算会阻塞主线程
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
console.log('计算完成');
});
// 这段代码要等上面的 Promise 回调执行完才能执行
setTimeout(() => {
console.log('定时器触发');
}, 0);
console.timeEnd('主线程执行');
requestAnimationFrame (RAF)
requestAnimationFrame
是主线程中的特殊异步 API,它与浏览器的渲染循环紧密相关。
javascript
// RAF 在浏览器重绘之前执行
let lastTime = 0;
function animate(currentTime) {
const delta = currentTime - lastTime;
lastTime = currentTime;
console.log(`距上一帧: ${delta.toFixed(2)}ms`);
// 更新动画状态
updateAnimation();
// 递归调用,持续动画
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
RAF 的特点:
- 执行时机:在浏览器重绘之前执行,通常每秒 60 次(60fps)
- 自动节流:页面不可见时会暂停执行,节省性能
- 高精度时间戳:回调接收 DOMHighResTimeStamp 参数
- 避免布局抖动:所有 DOM 修改在同一帧内批量处理
javascript
// RAF 的执行顺序
console.log('1 - 同步代码');
Promise.resolve().then(() => {
console.log('2 - 微任务');
});
requestAnimationFrame(() => {
console.log('3 - RAF');
});
setTimeout(() => {
console.log('4 - 宏任务');
}, 0);
// 输出顺序:
// 1 - 同步代码
// 2 - 微任务
// 3 - RAF
// 4 - 宏任务
requestIdleCallback (RIC)
requestIdleCallback
是另一个主线程专属的异步 API,用于在浏览器空闲时执行低优先级任务。
javascript
// RIC 在浏览器空闲时执行
requestIdleCallback((deadline) => {
console.log('空闲时间:', deadline.timeRemaining(), 'ms');
console.log('是否超时:', deadline.didTimeout);
// 在空闲时间内尽可能多地处理任务
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
const task = tasks.shift();
processTask(task);
}
// 如果还有任务,继续请求空闲回调
if (tasks.length > 0) {
requestIdleCallback(arguments.callee);
}
}, { timeout: 2000 }); // 最多等待 2 秒
RIC 的特点:
- 执行时机:在浏览器完成一帧渲染后,如果还有剩余时间
- 优先级最低:不会影响关键渲染和用户交互
- 可设置超时:避免任务被无限延迟
- 时间预算 :通过
deadline.timeRemaining()
获取剩余时间
javascript
// RIC 的典型应用:后台数据分析
const analyticsQueue = [];
function sendAnalytics(data) {
analyticsQueue.push(data);
// 使用 RIC 在空闲时发送
requestIdleCallback(() => {
if (analyticsQueue.length > 0) {
const batch = analyticsQueue.splice(0, 10);
fetch('/analytics', {
method: 'POST',
body: JSON.stringify(batch)
});
}
});
}
// 用户操作时收集数据,不影响性能
button.addEventListener('click', () => {
sendAnalytics({ event: 'click', timestamp: Date.now() });
});
主线程完整的执行顺序
综合以上所有异步 API,主线程的完整执行顺序如下:
javascript
┌─────────────── 一轮事件循环 ───────────────┐
│ │
│ 1. 执行同步代码(调用栈) │
│ └─ 清空调用栈 │
│ │
│ 2. 执行所有微任务 │
│ ├─ Promise.then/catch/finally │
│ ├─ async/await │
│ ├─ MutationObserver │
│ └─ queueMicrotask │
│ │
│ 3. 渲染阶段(如果需要渲染) │
│ ├─ 执行 requestAnimationFrame 回调 │
│ ├─ 计算样式(Style) │
│ ├─ 布局(Layout) │
│ └─ 绘制(Paint) │
│ │
│ 4. 执行一个宏任务 │
│ ├─ setTimeout/setInterval │
│ ├─ I/O 操作 │
│ ├─ UI 事件 │
│ └─ MessageChannel/postMessage │
│ │
│ 5. 如果有空闲时间 │
│ └─ 执行 requestIdleCallback 回调 │
│ │
└─────────────── 回到步骤 2 ─────────────────┘
完整示例:
javascript
console.log('1 - 同步代码');
setTimeout(() => {
console.log('2 - setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('3 - Promise');
});
queueMicrotask(() => {
console.log('4 - queueMicrotask');
});
requestAnimationFrame(() => {
console.log('5 - rAF');
});
requestIdleCallback(() => {
console.log('6 - rIC');
});
console.log('7 - 同步代码结束');
// 输出顺序:
// 1 - 同步代码
// 7 - 同步代码结束
// 3 - Promise
// 4 - queueMicrotask
// 5 - rAF
// 2 - setTimeout
// 6 - rIC (最后执行,且可能延迟较长时间)
微任务、宏任务、RAF 无限循环
javascript
// ❌ 微任务无限循环 - 会卡死浏览器
function infiniteMicrotask() {
Promise.resolve().then(() => {
infiniteMicrotask(); // 微任务队列永远无法清空
});
}
// 浏览器完全卡死,无法渲染,无法响应交互
// ⚠️ 宏任务无限循环 - 严重卡顿但不会完全卡死
function infiniteMacrotask() {
setTimeout(() => {
infiniteMacrotask(); // 每次执行后会清空微任务,可能渲染
}, 0);
}
// 浏览器严重卡顿,但理论上还有渲染和响应的机会
// ✅ RAF 无限循环 - 不会卡死,每帧执行一次
function infiniteRAF() {
requestAnimationFrame(() => {
infiniteRAF(); // 跟随浏览器渲染周期,约 60fps
});
}
// 浏览器不会卡死,每帧执行一次,其他任务仍能正常执行
原因: 事件循环必须清空所有微任务后才能进入渲染和下一个宏任务。如果微任务不断产生新的微任务,渲染永远无法执行。而宏任务每次只执行一个,执行间隙浏览器有机会渲染。RAF 则绑定在渲染周期上,每帧执行一次,不会阻塞其他任务。
Web Worker:并行执行模型
Web Worker 提供了真正的多线程并行执行能力,与主线程完全独立。
javascript
// 主线程
const worker = new Worker('worker.js');
console.log('主线程继续执行');
// 向 Worker 发送消息(不阻塞主线程)
worker.postMessage({ type: 'calculate', data: largeDataset });
// 接收 Worker 的消息
worker.addEventListener('message', (event) => {
console.log('Worker 返回结果:', event.data);
});
// worker.js(运行在独立线程)
self.addEventListener('message', (event) => {
const { type, data } = event.data;
if (type === 'calculate') {
// 密集计算不会阻塞主线程
const result = expensiveCalculation(data);
self.postMessage(result);
}
});
Web Worker 的限制
Web Worker 在技术实现上是线程(Thread) ,理论上是共享内存的,但它被设计成不共享内存的独立执行环境,无法访问主线程的许多 API。
❌ Web Worker 不支持的 API:
- DOM 操作
- 渲染相关: requestAnimationFrame/requestIdleCallback
- 存储相关: localStorage / sessionStorage
- window 对象的大部分属性
✅ Web Worker 支持的 API:
- 事件循环
- 网络请求
- IndexedDB
为什么 Web Worker 不允许操作 DOM?
- 线程安全问题:DOM 操作不是线程安全的,多线程同时修改 DOM 会导致竞态条件
- 复杂性控制:如果允许多线程访问 DOM,就需要引入锁机制,大大增加复杂性
- 性能考虑:频繁加锁会严重降低性能,违背了使用 Worker 的初衷
- JavaScript 哲学:JavaScript 从设计之初就是单线程模型,DOM 操作应该在主线程统一管理
因此,Web Worker 虽然是线程,但通过消息传递而非共享内存来通信,避免了传统多线程编程带来的问题。
Web Worker 的消息传递
主线程和 Worker 之间通过消息传递通信,有两种数据传递方式:
javascript
// 1. 结构化克隆(默认)- 数据被复制
const data = { numbers: [1, 2, 3, 4, 5] };
worker.postMessage(data);
data.numbers.push(6); // 不会影响 Worker 收到的数据
// 2. 转移所有权(Transferable)- 零拷贝
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(buffer, [buffer]); // 转移所有权
console.log(buffer.byteLength); // 0 - 主线程失去访问权
// Worker 中接收
self.addEventListener('message', (event) => {
const buffer = event.data;
console.log(buffer.byteLength); // 1048576 - Worker 获得所有权
});
使用 Transferable 的优势:
- 避免大数据的复制开销
- 适合处理大型 ArrayBuffer、ImageBitmap 等
- 性能更好,但主线程会失去访问权
总结
特性 | 主线程 | Web Worker |
---|---|---|
异步模型 | 并发(Concurrency) | 并行(Parallelism) |
线程 | 单线程 | 独立线程 |
事件循环 | 共享主线程的事件循环 | 拥有独立的事件循环 |
DOM 访问 | ✅ 完全支持 | ❌ 不支持 |
requestAnimationFrame | ✅ 支持 | ❌ 不支持 |
requestIdleCallback | ✅ 支持 | ❌ 不支持 |
localStorage | ✅ 支持 | ❌ 不支持 |
fetch / XMLHttpRequest | ✅ 支持 | ✅ 支持 |
setTimeout / Promise | ✅ 支持 | ✅ 支持 |
IndexedDB | ✅ 支持 | ✅ 支持 |
WebSocket | ✅ 支持 | ✅ 支持 |
通信方式 | 直接调用 | postMessage(消息传递) |
内存 | 共享 | 独立 |
适用场景 | UI 交互、渲染、轻量异步 | CPU 密集型计算 |
异步类型分类
1. 基于 Promise 的异步
任务队列:微任务(Micro Task)
Promise 是对异步操作的抽象,代表一个未来会完成的值。
javascript
// fetch API
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))
.finally(() => console.log('请求结束'));
// Promise 链式调用
Promise.resolve(1)
.then(value => value + 1)
.then(value => value * 2)
.then(value => console.log(value)); // 4
特点:
- 三种状态:pending、fulfilled、rejected
- 支持链式调用,避免回调地狱
- 统一的错误处理(catch)
- 可以组合(Promise.all、Promise.race 等)
- 微任务,优先级高于宏任务
2. async/await 异步
任务队列:微任务(Micro Task)
Promise 的语法糖,使异步代码看起来像同步代码。
javascript
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (error) {
console.error('请求失败:', error);
}
}
// 并行执行多个异步操作
async function fetchMultiple() {
const [data1, data2, data3] = await Promise.all([
fetch('/api/data1').then(r => r.json()),
fetch('/api/data2').then(r => r.json()),
fetch('/api/data3').then(r => r.json())
]);
return { data1, data2, data3 };
}
特点:
- 基于 Promise,本质是微任务
- 代码可读性高,接近同步代码
- 错误处理使用 try/catch
- 可以使用 await 等待 Promise 完成
- 函数必须声明为 async
3. 定时器类异步
任务队列:混合类型
setTimeout
/setInterval
:宏任务(Macro Task)requestAnimationFrame
:rAF 队列(渲染循环)requestIdleCallback
:rIC 队列(渲染循环)
基于时间的异步操作,包括 setTimeout、setInterval、requestAnimationFrame 等。
javascript
// setTimeout - 延迟执行一次
const timeoutId = setTimeout(() => {
console.log('1秒后执行');
}, 1000);
clearTimeout(timeoutId); // 取消
// setInterval - 重复执行
const intervalId = setInterval(() => {
console.log('每秒执行');
}, 1000);
clearInterval(intervalId); // 取消
// requestAnimationFrame - 浏览器下一次重绘前执行
function animate() {
// 更新动画
updateAnimation();
// 继续下一帧
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
// requestIdleCallback - 浏览器空闲时执行
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
performTask(tasks.shift());
}
});
特点:
- setTimeout/setInterval:宏任务,在下一轮事件循环执行
- requestAnimationFrame:在浏览器重绘前执行,约 60fps(16.67ms)
- requestIdleCallback:在浏览器空闲时执行,优先级最低
- 最小延迟限制(setTimeout 约 4ms)
4. 事件驱动的异步
任务队列:宏任务(Macro Task)
基于事件监听器的异步模式,广泛用于 DOM 交互和自定义事件。
javascript
// DOM 事件
document.addEventListener('click', (event) => {
console.log('页面被点击');
});
// 自定义事件
const eventTarget = new EventTarget();
eventTarget.addEventListener('custom', (event) => {
console.log('自定义事件触发:', event.detail);
});
eventTarget.dispatchEvent(new CustomEvent('custom', {
detail: { message: 'Hello' }
}));
// WebSocket 事件
const ws = new WebSocket('ws://example.com');
ws.addEventListener('open', () => {
console.log('连接建立');
});
ws.addEventListener('message', (event) => {
console.log('收到消息:', event.data);
});
ws.addEventListener('close', () => {
console.log('连接关闭');
});
特点:
- 基于观察者模式
- 可以有多个监听器
- 支持事件冒泡和捕获
- 需要手动清理,防止内存泄漏
5. Observer API
任务队列:混合类型
MutationObserver
:微任务(Micro Task)IntersectionObserver
/ResizeObserver
/PerformanceObserver
:宏任务(Macro Task)
现代浏览器提供的观察者 API,用于监听特定类型的变化。
javascript
// MutationObserver - 监听 DOM 变化
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('DOM 变化:', mutation.type);
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true
});
// IntersectionObserver - 监听元素可见性
const intersectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素进入视口');
lazyLoadImage(entry.target);
}
});
});
intersectionObserver.observe(document.querySelector('.lazy-image'));
// ResizeObserver - 监听元素尺寸变化
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
console.log('元素尺寸变化:', entry.contentRect);
});
});
resizeObserver.observe(document.querySelector('.resizable'));
特点:
- 异步执行,批量处理变化
- 性能优于轮询
- 需要手动断开连接(disconnect)
6. Worker 类异步
任务队列:独立线程(不在事件循环中)
在独立线程中执行代码,真正的并行计算。Worker 的消息回调是宏任务。
javascript
// Web Worker - 独立线程
const worker = new Worker('worker.js');
worker.postMessage({ type: 'start', data: largeData });
worker.addEventListener('message', (event) => {
console.log('Worker 返回结果:', event.data);
});
worker.addEventListener('error', (event) => {
console.error('Worker 错误:', event.message);
});
// 终止 worker
worker.terminate();
// worker.js
self.addEventListener('message', (event) => {
const result = heavyComputation(event.data);
self.postMessage(result);
});
特点:
- 真正的多线程并行
- 不能直接访问 DOM
- 通过消息传递通信
- 独立的全局作用域
- 适合 CPU 密集型任务
7. 流式异步(Streams)
任务队列:微任务(Micro Task)
处理大量数据的流式接口。Stream 的读取操作基于 Promise,因此是微任务。
javascript
// ReadableStream - 读取流
async function processStream(url) {
const response = await fetch(url);
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('流读取完成');
break;
}
console.log('收到数据块:', value.length, '字节');
processChunk(value);
}
}
// TransformStream - 转换流
const transformStream = new TransformStream({
transform(chunk, controller) {
// 转换数据
const transformed = chunk.toUpperCase();
controller.enqueue(transformed);
}
});
// 管道连接
fetch('/data')
.then(response => response.body)
.then(body => body.pipeThrough(transformStream))
.then(stream => stream.pipeTo(writableStream));
特点:
- 逐块处理数据,内存效率高
- 支持背压(backpressure)
- 可以组合和转换
- 适合处理大文件或实时数据
8. Generator 与 Async Generator
任务队列:取决于具体实现
Generator
:同步执行(在调用栈中)Async Generator
:微任务(基于 Promise)
可以暂停和恢复执行的函数。
javascript
// Generator - 同步迭代
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
// Async Generator - 异步迭代
async function* asyncNumberGenerator() {
yield await fetch('/api/1').then(r => r.json());
yield await fetch('/api/2').then(r => r.json());
yield await fetch('/api/3').then(r => r.json());
}
(async () => {
for await (const data of asyncNumberGenerator()) {
console.log(data);
}
})();
特点:
- 可以暂停和恢复执行
- 懒加载,按需生成值
- Async Generator 结合了 Promise 和 Generator
- 适合处理序列数据