JavaScript 是单线程语言,意味着同一时间只能执行一个任务。如果所有任务都同步执行,遇到网络请求、文件读取这类耗时操作时,页面会卡住(阻塞),用户体验极差。
异步编程的核心就是「不等待耗时操作完成,继续执行后续任务,等耗时操作有结果后再回头处理」。比如我们用 fetch 请求接口数据时,不会一直等着数据返回,而是继续执行下面的代码,等数据返回后再通过回调函数处理结果。
前端常用的异步场景有:网络请求(AJAX/fetch)、定时器(setTimeout/setInterval)、事件监听、文件读写等。
最早期的异步实现方式是回调函数,比如:
// 定时器回调
setTimeout(() => {
console.log('1秒后执行');
}, 1000);
// 事件监听回调
document.getElementById('btn').addEventListener('click', () => {
console.log('按钮被点击');
});
回调函数简单直观,但遇到多个依赖的异步操作时,会出现「回调地狱」(Callback Hell)—— 嵌套层级越来越深,代码可读性和可维护性极差。
示例:多个依赖的网络请求
// 先请求用户信息,再根据用户ID请求订单列表,再根据订单ID请求订单详情
$.get('/api/user', (user) => {
$.get(`/api/orders?userId=${user.id}`, (orders) => {
$.get(`/api/orderDetail?orderId=${orders[0].id}`, (detail) => {
console.log('订单详情', detail);
}, (err) => {
console.error('请求订单详情失败', err);
});
}, (err) => {
console.error('请求订单列表失败', err);
});
}, (err) => {
console.error('请求用户信息失败', err);
});
上面的代码嵌套了3层回调,看起来像「金字塔」,后续维护时要一层层找逻辑,非常痛苦。
Promise 是 ES6 引入的异步编程解决方案,它把异步操作封装成一个「承诺」对象,用链式调用替代嵌套回调,解决了回调地狱的问题。
Promise 的核心特性:
pending(进行中)、fulfilled(成功)、rejected(失败)then() 方法用于处理成功结果,catch() 方法用于处理失败结果示例1:创建 Promise 对象
const promise = new Promise((resolve, reject) => {
// 异步操作:这里用定时器模拟网络请求
setTimeout(() => {
const success = true;
if (success) {
// 成功时调用 resolve,传递结果
resolve('请求成功的数据');
} else {
// 失败时调用 reject,传递错误信息
reject(new Error('请求失败'));
}
}, 1000);
});
示例2:用 Promise 解决回调地狱
// 先把异步请求封装成 Promise
function getUser() {
return new Promise((resolve, reject) => {
$.get('/api/user', resolve, reject);
});
}
function getOrders(userId) {
return new Promise((resolve, reject) => {
$.get(`/api/orders?userId=${userId}`, resolve, reject);
});
}
function getOrderDetail(orderId) {
return new Promise((resolve, reject) => {
$.get(`/api/orderDetail?orderId=${orderId}`, resolve, reject);
});
}
// 链式调用,替代嵌套
getUser()
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => console.log('订单详情', detail))
.catch(err => console.error('请求失败', err)); // 统一捕获所有错误
链式调用把「金字塔」变成了「扁平结构」,逻辑清晰了很多,而且可以用一个 catch() 统一捕获所有环节的错误,不用每个回调都写错误处理。
Promise 常用静态方法:
Promise.all([p1, p2, p3]):接收一个 Promise 数组,所有 Promise 都成功才返回成功结果数组;只要有一个失败,就立即返回失败信息。适用于「多个独立异步操作,需要全部完成后再处理」的场景(比如同时请求多个接口数据)。Promise.race([p1, p2, p3]):接收一个 Promise 数组,只要有一个 Promise 状态改变(成功或失败),就立即返回该结果。适用于「超时控制」场景(比如请求接口超过3秒就提示超时)。Promise.resolve(data):快速创建一个成功状态的 Promise,直接传递数据。Promise.reject(err):快速创建一个失败状态的 Promise,直接传递错误信息。
Promise 解决了回调地狱,但链式调用依然需要写 then(),代码还是不够直观。Async/Await 是 ES2017 引入的语法糖,基于 Promise 实现,让异步代码看起来像同步代码一样简洁。
Async/Await 的核心用法:
async 关键字修饰函数,该函数会自动返回一个 Promise 对象async 函数内部,用 await 关键字等待 Promise 完成(只能在 async 函数内部使用)try/catch 捕获异步操作的错误(替代 Promise 的 catch())示例:用 Async/Await 重写之前的嵌套请求
// 复用之前封装好的 Promise 函数
function getUser() { /* ... */ }
function getOrders(userId) { /* ... */ }
function getOrderDetail(orderId) { /* ... */ }
// 用 async/await 编写异步代码
async function getOrderInfo() {
try {
const user = await getUser(); // 等待获取用户信息
const orders = await getOrders(user.id); // 等待获取订单列表
const detail = await getOrderDetail(orders[0].id); // 等待获取订单详情
console.log('订单详情', detail);
return detail; // async 函数返回的结果会被包装成 Promise 成功值
} catch (err) {
console.error('请求失败', err); // 捕获所有异步操作的错误
throw err; // 可选:把错误抛出去,让调用者可以继续处理
}
}
// 调用 async 函数
getOrderInfo();
上面的代码完全没有嵌套和 then(),看起来和同步代码几乎一样,可读性大大提升。这也是目前前端异步编程的主流方案。
Async/Await 高级技巧:并行执行异步操作
注意:如果直接用多个 await 执行独立的异步操作,会变成串行执行(上一个完成才执行下一个),效率很低。如果多个异步操作没有依赖关系,应该用 Promise.all() 并行执行。
// 错误示范:串行执行,总耗时 = 1秒 + 2秒 + 3秒 = 6秒
async function getMultiData() {
const data1 = await new Promise(resolve => setTimeout(() => resolve('数据1'), 1000));
const data2 = await new Promise(resolve => setTimeout(() => resolve('数据2'), 2000));
const data3 = await new Promise(resolve => setTimeout(() => resolve('数据3'), 3000));
return [data1, data2, data3];
}
// 正确示范:并行执行,总耗时 = 3秒(取最长的异步操作时间)
async function getMultiData() {
// 先创建所有 Promise(立即执行)
const promise1 = new Promise(resolve => setTimeout(() => resolve('数据1'), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve('数据2'), 2000));
const promise3 = new Promise(resolve => setTimeout(() => resolve('数据3'), 3000));
// 同时等待所有 Promise 完成
const [data1, data2, data3] = await Promise.all([promise1, promise2, promise3]);
return [data1, data2, data3];
}
Promise 如果不写 catch(),Async/Await 如果不写 try/catch,异步操作的错误会被忽略(在浏览器控制台会报 Uncaught (in promise) Error),导致程序隐藏 bug。
解决方案:
catch() 捕获错误try/catch 包裹 await 操作// 全局捕获未处理的 Promise 错误
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的 Promise 错误', event.reason);
event.preventDefault(); // 阻止浏览器默认报错
});
如前所述,没有依赖关系的异步操作,不要用多个 await 串行执行,要用 Promise.all() 并行执行,提升效率。
Promise.all() 并行执行Promise.all() + map() 替代JavaScript 异步编程的演进是一个「从复杂到简洁」的过程:回调函数解决了异步执行的问题,但带来了回调地狱;Promise 用链式调用解决了回调地狱,但代码仍有冗余;Async/Await 基于 Promise 实现,让异步代码变得像同步代码一样直观。
实际开发中,建议: