引言
JavaScript 作为一门单线程语言,异步编程是其核心特性之一。从早期的回调函数到 Promise 对象,再到 ES2017 引入的 async/await 语法糖,JavaScript 异步编程的写法经历了巨大的演进。本文将系统梳理 JavaScript 异步编程的发展脉络,深入讲解 Promise 与 async/await 的原理与实践,并总结日常开发中的最佳实践。
一、回调函数时代的问题
在早期的 JavaScript 中,处理异步操作主要依赖回调函数。例如读取文件、发起网络请求等操作,都需要传入一个回调函数来处理结果:
// 典型的回调函数写法
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: 'example' };
callback(null, data);
}, 1000);
}
fetchData((err, data) => {
if (err) {
console.error('出错了:', err);
} else {
console.log('获取到数据:', data);
}
});
当多个异步操作需要依次执行时,就会出现著名的"回调地狱"(Callback Hell):
// 回调地狱示例
getUserData(userId, (err, user) => {
if (err) { handleError(err); return; }
getOrders(user.id, (err, orders) => {
if (err) { handleError(err); return; }
getOrderDetail(orders[0].id, (err, detail) => {
if (err) { handleError(err); return; }
// 更多嵌套...
});
});
});
这种写法的问题很明显:代码横向发展而非纵向,可读性差、错误处理复杂、难以维护和调试。
二、Promise:异步编程的救星
2.1 Promise 基本概念
Promise 是 ES6 引入的异步编程解决方案,它代表一个异步操作的最终完成或失败。Promise 有三种状态:
- pending(进行中):初始状态,既不是成功也不是失败
- fulfilled(已成功):操作成功完成
- rejected(已失败):操作失败
一旦 Promise 的状态从 pending 变为 fulfilled 或 rejected,就不会再改变,此时称为 settled(已定型)。
2.2 创建和消费 Promise
// 创建一个 Promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('操作成功');
} else {
reject(new Error('操作失败'));
}
}, 1000);
});
// 消费 Promise
myPromise
.then(result => {
console.log(result); // 操作成功
return processResult(result);
})
.then(processed => {
console.log(processed);
})
.catch(error => {
console.error('捕获错误:', error.message);
})
.finally(() => {
console.log('无论成功失败都会执行');
});
2.3 Promise 链式调用
Promise 最大的优势之一是通过链式调用解决了回调地狱问题:
// 使用 Promise 链式调用替代回调地狱
getUserData(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => {
console.log('订单详情:', detail);
})
.catch(err => {
console.error('任何一步出错都会被捕获:', err);
});
链式调用的优势是代码呈纵向发展,结构清晰,并且只需在最末尾统一处理错误。
2.4 Promise 常用静态方法
Promise.all — 等待所有 Promise 完成:
const fetchUser = fetch('/api/user/1').then(r => r.json());
const fetchPosts = fetch('/api/posts?userId=1').then(r => r.json());
const fetchComments = fetch('/api/comments?postId=1').then(r => r.json());
// 并行请求,全部完成后统一处理
Promise.all([fetchUser, fetchPosts, fetchComments])
.then(([user, posts, comments]) => {
console.log('用户:', user);
console.log('文章:', posts);
console.log('评论:', comments);
})
.catch(err => {
console.error('任一请求失败:', err);
});
Promise.race — 返回最快完成的 Promise 结果:
// 请求超时控制
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), 5000)
);
Promise.race([fetch('/api/data'), timeout])
.then(data => console.log('请求成功:', data))
.catch(err => console.error(err.message)); // 5秒后输出"请求超时"
Promise.allSettled — 等待所有 Promise 完成(无论成功失败):
const urls = ['/api/a', '/api/b', '/api/c'];
const requests = urls.map(url =>
fetch(url).then(r => r.json()).catch(err => ({ error: err.message }))
);
Promise.allSettled(requests)
.then(results => {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`${urls[index]} 成功:`, result.value);
} else {
console.log(`${urls[index]} 失败:`, result.reason);
}
});
});
Promise.any — 返回第一个成功的 Promise:
const mirrors = [
'https://mirror1.example.com/api/data',
'https://mirror2.example.com/api/data',
'https://mirror3.example.com/api/data'
];
Promise.any(mirrors.map(url => fetch(url)))
.then(response => console.log('最快响应:', response))
.catch(err => console.error('所有请求失败:', err));
三、async/await:异步编程的优雅写法
3.1 基本语法
async/await 是 ES2017 引入的语法糖,让异步代码看起来像同步代码一样直观:
// Promise 写法
function getOrderInfo() {
return getUserId()
.then(id => getUser(id))
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => {
console.log(detail);
return detail;
})
.catch(err => {
console.error(err);
});
}
// async/await 写法(逻辑完全等价)
async function getOrderInfo() {
try {
const id = await getUserId();
const user = await getUser(id);
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
console.log(detail);
return detail;
} catch (err) {
console.error(err);
}
}
使用 async/await 后,代码的流程和逻辑一目了然,不再需要层层嵌套的 .then() 调用。
3.2 错误处理模式
在 async/await 中,错误处理主要使用 try/catch:
// 模式一:顶层 try/catch
async function fetchData() {
try {
const data = await fetch('/api/data').then(r => r.json());
return data;
} catch (err) {
console.error('请求失败:', err);
throw err; // 向上传播
}
}
// 模式二:单个 await 的 try/catch
async function fetchMixed() {
let cachedData;
try {
cachedData = await loadFromCache();
} catch {
cachedData = null; // 缓存加载失败不阻断后续流程
}
const freshData = await fetchFreshData();
return { ...cachedData, ...freshData };
}
// 模式三:封装为 Result 类型
async function safeFetch(url) {
try {
const response = await fetch(url);
const data = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: error.message };
}
}
// 使用
const result = await safeFetch('/api/user');
if (result.success) {
console.log(result.data);
} else {
console.error('出错:', result.error);
}
3.3 并发执行优化
async/await 虽然让代码看起来是同步执行,但如果多个异步操作之间没有依赖关系,仍然需要使用 Promise.all 并行执行:
// 错误写法:串行执行(总耗时约 3 秒)
async function getDashboardData_bad() {
const users = await fetchUsers(); // 等待 1 秒
const orders = await fetchOrders(); // 等待 1 秒
const stats = await fetchStats(); // 等待 1 秒
return { users, orders, stats };
}
// 正确写法:并行执行(总耗时约 1 秒)
async function getDashboardData_good() {
const [users, orders, stats] = await Promise.all([
fetchUsers(),
fetchOrders(),
fetchStats()
]);
return { users, orders, stats };
}
// 混合写法:部分有依赖,部分可并行
async function getReport() {
const user = await fetchUser(userId); // 先获取用户
const [orders, preferences] = await Promise.all([
fetchOrders(user.id), // 并行
fetchPreferences(user.id) // 并行
]);
return generateReport(user, orders, preferences);
}
四、实战场景
4.1 带重试的网络请求
async function fetchWithRetry(url, options = {}, maxRetries = 3, delay = 1000) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
if (attempt === maxRetries) throw error;
console.warn(`第 ${attempt + 1} 次重试...`);
await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
}
}
}
// 使用:最多重试3次,间隔递增
const data = await fetchWithRetry('/api/critical-data', {}, 3, 1000);
4.2 异步迭代器与异步生成器
// 异步生成器:按需分页获取数据
async function* fetchPaginatedData(baseUrl) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${baseUrl}?page=${page}`);
const data = await response.json();
yield data.items; // 每次 yield一页数据
hasMore = data.hasNextPage;
page++;
}
}
// 消费异步生成器
async function processAllPages() {
const generator = fetchPaginatedData('/api/articles');
for await (const items of generator) {
for (const item of items) {
await processItem(item);
}
}
}
4.3 Promise 取消与 AbortController
// 使用 AbortController 取消异步请求
let controller;
async function startLongRequest() {
controller = new AbortController();
const { signal } = controller;
try {
const response = fetch('/api/heavy-computation', { signal });
const data = await response;
return data;
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求已被取消');
} else {
throw err;
}
}
}
// 取消请求
function cancelRequest() {
if (controller) {
controller.abort();
}
}
// 实战应用:搜索防抖 + 取消旧请求
let searchController;
async function handleSearch(keyword) {
if (searchController) {
searchController.abort(); // 取消之前的搜索
}
searchController = new AbortController();
try {
const results = await fetch(`/api/search?q=${keyword}`, {
signal: searchController.signal
});
renderResults(await results.json());
} catch (err) {
if (err.name !== 'AbortError') throw err;
}
}
五、最佳实践总结
- 优先使用 async/await:代码更清晰直观,便于维护和调试
- 善用 Promise.all:无依赖的异步操作一律并行执行
- 记住 Promise 一旦创建就立即执行:如果不希望立即执行,将其包装在函数中
- 永远不要在非 async 函数中遗漏 .catch():未捕获的 Promise 拒绝可能导致程序静默失败
- 循环中的 async 注意性能:for 循环中 await 是串行,如需并行使用 Promise.all + map
- async 函数始终返回 Promise:即使 return 一个普通值,也会被自动包装为 Promise
- 使用 AbortController 配合场景:长时间运行的异步操作(大文件上传、复杂计算)应支持取消
结语
JavaScript 异步编程从回调函数到 Promise 再到 async/await,每一步演进都是为了让代码更优雅、更可靠。理解 Promise 的底层机制(状态机、微任务队列)对于写出健壮的异步代码至关重要,而 async/await 则是实践中最高效的编码方式。在实际项目中,合理运用本文介绍的模式和技巧,能够显著提升代码质量和开发效率。

发表评论 取消回复