JavaScript 异步编程
大约 7 分钟约 2196 字
JavaScript 异步编程
简介
JavaScript 是单线程语言,通过事件循环(Event Loop)和异步机制处理并发任务。理解 Promise、async/await、微任务和宏任务的执行顺序,是编写高性能前端代码的关键。异步编程从回调函数(Callback)发展到 Promise 链式调用,再到 async/await 同步写法,代码可读性不断提升。深入理解 Event Loop 的调度机制、并发控制、AbortController 取消和异步迭代器,有助于处理复杂的异步场景。
特点
实现
Promise 与 async/await
// ========== Promise 基础 ==========
function fetchUser(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id, name: '张三', email: 'zhangsan@example.com' });
} else {
reject(new Error('无效的用户 ID'));
}
}, 100);
});
}
// Promise 三种状态:pending → fulfilled / rejected
// 状态一旦改变不可逆
// ========== async/await — 更直观的异步写法 ==========
async function loadUserProfile(userId) {
try {
// 并行请求 — 避免 await 串行
const [user, posts, settings] = await Promise.all([
fetchUser(userId),
fetchPosts(userId),
fetchSettings(userId),
]);
return { user, posts, settings };
} catch (error) {
console.error('加载用户资料失败:', error.message);
throw error;
}
}
// ========== 链式调用 ==========
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => console.log(posts))
.catch(err => console.error(err))
.finally(() => console.log('请求完成'));
// ========== Promise 静态方法 ==========
// Promise.resolve — 立即 resolved
const resolved = await Promise.resolve(42);
// Promise.reject — 立即 rejected
try { await Promise.reject(new Error('test')); } catch (e) {}
// Promise.all — 全部成功才成功,一个失败整体失败
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json()),
]);
// Promise.allSettled — 全部完成,不管成功失败
const results = await Promise.allSettled([
fetch('/api/a').then(r => r.json()),
fetch('/api/b').then(r => r.json()),
fetch('/api/c').then(r => r.json()),
]);
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.error('失败:', result.reason);
}
});
// Promise.race — 取最快的结果(成功或失败)
const fastest = await Promise.race([
fetch('/cdn1/data.json'),
fetch('/cdn2/data.json'),
]);
// Promise.any — 取第一个成功的结果(全部失败才 reject)
const firstSuccess = await Promise.any([
fetch('/api/primary').then(r => r.json()),
fetch('/api/backup').then(r => r.json()),
]);Event Loop 执行顺序
// ========== Event Loop 执行顺序示例 ==========
console.log('1. 同步代码');
setTimeout(() => {
console.log('5. 宏任务 setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('3. 微任务 Promise.then');
}).then(() => {
console.log('4. 微任务链式 then');
});
console.log('2. 同步代码');
// 输出顺序:1 → 2 → 3 → 4 → 5
// 解释:
// 1. 执行同步代码(1, 2)
// 2. 清空微任务队列(3, 4)
// 3. 取一个宏任务执行(5)
// ========== 宏任务和微任务 ==========
// 宏任务(Macrotask):
// setTimeout, setInterval, setImmediate (Node.js)
// I/O 操作, UI 渲染
// requestAnimationFrame
// 微任务(Microtask):
// Promise.then/catch/finally
// queueMicrotask
// MutationObserver
// async/await (await 后面的代码是微任务)
// ========== 复杂示例 ==========
async function asyncFunction() {
console.log('A'); // 同步
await Promise.resolve(); // await 后面的代码变成微任务
console.log('B'); // 微任务
setTimeout(() => console.log('C'), 0); // 宏任务
Promise.resolve().then(() => console.log('D')); // 微任务
console.log('E'); // 同步
}
asyncFunction();
console.log('F'); // 同步
// 输出:A → F → B → E → D → C
// ========== 微任务优先级 ==========
// 在同一个事件循环中,微任务总是优先于宏任务执行
// 每次执行完一个宏任务后,都会清空所有微任务并发控制
// ========== 并发限制器 ==========
async function promiseLimit(tasks, limit = 3) {
const results = [];
const executing = new Set();
for (const task of tasks) {
const p = task().then(result => {
executing.delete(p);
return result;
});
executing.add(p);
results.push(p);
// 如果达到并发限制,等待一个完成
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// 使用 — 100 个请求,同时最多 5 个
const tasks = Array.from({ length: 100 }, (_, i) =>
() => fetch(`/api/items/${i}`).then(r => r.json())
);
const results = await promiseLimit(tasks, 5);
// ========== 带重试的并发请求 ==========
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let i = 0; i <= retries; i++) {
try {
const res = await fetch(url, options);
if (!res.ok && res.status >= 500 && i < retries) {
// 服务器错误且还有重试次数
await new Promise(r => setTimeout(r, delay * (i + 1))); // 指数退避
continue;
}
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (error) {
if (i === retries) throw error;
if (error.name === 'AbortError') throw error; // 取消不重试
await new Promise(r => setTimeout(r, delay * (i + 1)));
}
}
}
// ========== 请求去重 ==========
const pendingRequests = new Map();
function deduplicatedFetch(url, options = {}) {
const key = `${url}:${JSON.stringify(options)}`;
if (pendingRequests.has(key)) {
return pendingRequests.get(key);
}
const promise = fetch(url, options)
.finally(() => pendingRequests.delete(key));
pendingRequests.set(key, promise);
return promise;
}AbortController 取消请求
// ========== AbortController 基础 ==========
const controller = new AbortController();
const signal = controller.signal;
fetch('/api/data', { signal })
.then(r => r.json())
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求已取消');
}
});
// 取消请求
controller.abort();
// ========== 超时取消 ==========
function fetchWithTimeout(url, timeout = 10000, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { ...options, signal: controller.signal })
.finally(() => clearTimeout(timeoutId));
}
// ========== React 中的取消 ==========
function UserComponent({ userId }) {
const abortRef = useRef(null);
useEffect(() => {
const controller = new AbortController();
abortRef.current = controller;
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(r => r.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err);
});
return () => controller.abort(); // 组件卸载时取消
}, [userId]);
}
// ========== 取消多个请求 ==========
class RequestManager {
constructor() {
this.controllers = new Map();
}
create(key) {
// 取消同 key 的旧请求
this.cancel(key);
const controller = new AbortController();
this.controllers.set(key, controller);
return controller.signal;
}
cancel(key) {
this.controllers.get(key)?.abort();
this.controllers.delete(key);
}
cancelAll() {
this.controllers.forEach(c => c.abort());
this.controllers.clear();
}
}
const requestManager = new RequestManager();异步迭代与 AsyncGenerator
// ========== AsyncGenerator — 分页请求 ==========
async function* paginate(apiUrl, pageSize = 20) {
let url = apiUrl;
let page = 1;
while (url) {
const res = await fetch(`${url}?page=${page}&pageSize=${pageSize}`);
const data = await res.json();
yield {
items: data.items,
page: data.page,
total: data.total,
hasMore: data.hasMore,
};
if (!data.hasMore) break;
page++;
}
}
// ========== for await...of ==========
async function processAllUsers() {
for await (const page of paginate('/api/users')) {
console.log(`第 ${page.page} 页: ${page.items.length} 条`);
// 处理当前页数据...
}
}
// ========== 实时数据流 ==========
async function* streamData(url) {
const res = await fetch(url);
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(Boolean);
for (const line of lines) {
if (line.startsWith('data: ')) {
yield JSON.parse(line.slice(6));
}
}
}
}
// 使用 — 处理 Server-Sent Events
for await (const event of streamData('/api/events')) {
console.log('事件:', event);
}
// ========== 限速异步迭代 ==========
async function* throttle(iterable, delay) {
for await (const item of iterable) {
yield item;
await new Promise(r => setTimeout(r, delay));
}
}
// 使用
for await (const user of throttle(paginate('/api/users'), 100)) {
renderUser(user);
}实用异步工具函数
// ========== delay — 延迟 ==========
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await delay(1000); // 等待 1 秒
// ========== retry — 重试 ==========
async function retry(fn, retries = 3, delayMs = 1000) {
for (let i = 0; i <= retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries) throw error;
await delay(delayMs * Math.pow(2, i)); // 指数退避
}
}
}
// ========== timeout — 超时包装 ==========
async function timeout(promise, ms) {
const timer = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
);
return Promise.race([promise, timer]);
}
// ========== debounce — 异步防抖 ==========
function asyncDebounce(fn, delay) {
let timer;
let pendingResolve;
return async (...args) => {
clearTimeout(timer);
return new Promise(resolve => {
pendingResolve = resolve;
timer = setTimeout(async () => {
try {
const result = await fn(...args);
resolve(result);
} catch (error) {
reject(error);
}
}, delay);
});
};
}
const searchUsers = asyncDebounce(async (keyword) => {
const res = await fetch(`/api/search?q=${keyword}`);
return res.json();
}, 300);
// ========== queue — 异步队列 ==========
class AsyncQueue {
constructor(concurrency = 1) {
this.concurrency = concurrency;
this.running = 0;
this.queue = [];
}
async add(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.run();
});
}
async run() {
while (this.running < this.concurrency && this.queue.length > 0) {
const { fn, resolve, reject } = this.queue.shift();
this.running++;
try {
resolve(await fn());
} catch (error) {
reject(error);
} finally {
this.running--;
this.run();
}
}
}
}优点
缺点
总结
Promise 是 JavaScript 异步编程的基础,async/await 提供更直观的语法。Event Loop 先清空微任务队列,再取一个宏任务。Promise.all 并行请求,Promise.allSettled 容错处理。AbortController 实现请求取消。AsyncGenerator 处理分页和流式数据。并发限制器控制同时请求数量。
关键知识点
- Event Loop 先清空微任务队列,再取一个宏任务。
- await 后面的代码相当于 .then 回调,是微任务。
- Promise.all 只要有一个失败就整体失败,allSettled 等待全部完成。
- async 函数总是返回 Promise。
- AbortController 可以取消 fetch 请求。
项目落地视角
- 并行请求使用 Promise.all,避免串行 await。
- 所有异步操作添加错误处理(try/catch 或 .catch)。
- 长时间请求使用 AbortController 支持取消。
常见误区
- 在循环中使用 await 导致本可并行的请求串行执行。
- 忘记 .catch() 导致 Unhandled Promise Rejection。
- 在 Promise 构造函数中 throw 而非 reject。
进阶路线
- 深入学习 Event Loop、Job Queue 和 V8 引擎调度机制。
- 研究异步函数的编译转换(regenerator-runtime)。
- 学习 RxJS 的响应式异步编程模型。
适用场景
- API 请求和数据处理。
- 文件读取和流处理。
- 定时任务和动画帧调度。
落地建议
- 封装统一的请求工具函数处理错误和重试。
- 使用 AbortController 管理请求生命周期。
- 区分可并行和必须串行的异步操作。
排错清单
- 检查是否有未捕获的 Promise rejection。
- 确认 async 函数调用是否添加了 await。
- 检查竞态条件(快速连续请求的时序问题)。
复盘问题
- 你的项目中串行 await 有多少可以改为 Promise.all 并行?
- 如何处理多个请求中部分失败的降级策略?
- AbortController 在组件卸载时是否正确清理?
