在前端面试的技术环节中,异步编程绝对是绕不开的核心考点,而Promise与async/await作为现代JavaScript异步编程的基石,更是面试官重点考察的内容。从"回调地狱"的痛点出发,到Promise的规范落地,再到async/await的语法优化,这三者的演进脉络不仅体现了JavaScript的发展方向,更藏着前端工程师必备的工程化思维。
一、为什么需要Promise?------ 从"回调地狱"说起
在Promise出现之前,JavaScript的异步操作全靠回调函数实现。比如我们需要依次执行"请求用户信息→根据用户ID请求订单→根据订单ID请求商品详情"这一系列异步操作,代码会变成这样:
javascript
// 回调地狱示例
requestUserInfo(userId, function(user) {
requestOrder(user.orderId, function(order) {
requestGoods(order.goodsId, function(goods) {
// 业务逻辑
console.log(goods);
}, function(err) {
console.error('请求商品失败', err);
});
}, function(err) {
console.error('请求订单失败', err);
});
}, function(err) {
console.error('请求用户失败', err);
});
这种嵌套层级不断加深的代码被称为"回调地狱",它存在三个致命问题:可读性差 (代码呈"金字塔"结构,难以快速梳理执行流程)、可维护性低 (修改中间某一步逻辑需要逐层定位)、错误处理混乱(每个回调都要单独处理错误,无法统一捕获)。
为了解决这些问题,ES6正式引入了Promise规范,它通过"状态管理"的方式将异步操作的结果与后续逻辑解耦,让异步代码变得线性、清晰。
二、Promise核心解析:状态、方法与原理
Promise的本质是一个异步操作的状态管理器,它用标准化的API封装异步操作,让开发者可以专注于业务逻辑而非异步流程控制。要掌握Promise,首先要吃透它的"状态机制"。
1. 三个核心状态与状态不可逆性
Promise有且只有三个状态,且状态一旦改变就会永久固定,无法再修改:
- pending(等待中) :初始状态,异步操作尚未完成。此时Promise既不成功也不失败。
- fulfilled(已成功) :异步操作完成且结果有效。状态从pending转为fulfilled后,会触发后续的then回调。
- rejected(已失败) :异步操作失败或结果无效。状态从pending转为rejected后,会触发后续的catch回调。
面试高频考点:Promise状态不可逆的特性。比如"状态从fulfilled转为rejected是否可行?"答案是不行,一旦状态确定就无法变更。曾有面试题用setTimeout模拟异步,考察对状态固定的理解,务必注意。
2. 基本用法:创建与链式调用
Promise通过构造函数创建,接收一个 executor 函数作为参数,该函数有两个内置参数resolve和reject,分别用于将状态转为fulfilled和rejected:
js
// Promise基本用法
const promise = new Promise((resolve, reject) => {
// 模拟异步操作(比如接口请求)
setTimeout(() => {
const success = true;
if (success) {
// 成功:传递结果给resolve
resolve('异步操作成功的结果');
} else {
// 失败:传递错误信息给reject
reject(new Error('异步操作失败'));
}
}, 1000);
});
// 链式调用处理结果
promise
.then(result => {
console.log('成功接收结果:', result);
return result + ',已处理'; // 传递给下一个then
})
.then(processedResult => {
console.log('处理后的结果:', processedResult);
})
.catch(error => {
console.error('捕获错误:', error);
});
这里有两个关键知识点:
- then方法的返回值:then方法会返回一个新的Promise对象,这是链式调用的核心。如果then中返回一个非Promise值,会自动包装成fulfilled状态的Promise;如果返回一个Promise,则后续then会等待该Promise状态变更。
- catch的作用:catch用于捕获前面所有链式调用中的错误,包括Promise构造函数中reject的错误和then回调中抛出的异常,实现"统一错误处理"。
3. 常用静态方法:all、race、allSettled
除了实例方法,Promise的静态方法在实际开发和面试中都高频出现,重点掌握以下三个:
(1)Promise.all(iterable)
接收一个可迭代对象(如数组),里面包含多个Promise。它的核心特性是"全部成功才成功,一个失败则整体失败":
js
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then(results => {
console.log(results); // [1, 2, 3](按传入顺序返回结果)
})
.catch(error => {
console.error(error); // 若任意一个Promise reject,立即触发catch
});
面试考点:如果Promise.all中某个Promise失败,其他Promise还会继续执行吗?答案是会的。因为Promise状态一旦触发就无法中断,all只是"监听"状态,不会干预单个Promise的执行。
(2)Promise.race(iterable)
同样接收可迭代对象,核心特性是"谁先完成谁生效"------只要有一个Promise状态变更(成功或失败),就立即返回该结果,忽略其他Promise的后续状态:
js
// 模拟接口请求超时控制
const request = new Promise((resolve) => {
setTimeout(() => resolve('请求成功'), 3000);
});
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error('请求超时')), 2000);
});
Promise.race([request, timeout])
.then(result => console.log(result))
.catch(error => console.error(error)); // 2秒后输出"请求超时"
实际用途:常见于接口请求超时控制、资源加载竞争等场景。
(3)Promise.allSettled(iterable)
ES2020新增方法,与all的"一错全错"不同,它会等待所有Promise都完成(无论成功或失败) ,然后返回一个包含所有结果的数组,每个元素包含对应Promise的状态和结果:
js
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject(new Error('失败'));
Promise.allSettled([promise1, promise2])
.then(results => {
console.log(results);
// 输出:
// [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: Error: 失败 }
// ]
});
适用场景:需要获取所有异步操作的完整结果,比如批量请求数据时,即使部分失败也需要知道失败原因。
三、async/await:Promise的"语法糖"与最佳实践
虽然Promise解决了回调地狱,但链式调用过多时,代码依然会存在"then链"的冗余。ES2017引入的async/await,通过"同步语法写异步逻辑"的方式,进一步简化了Promise的使用,成为当前异步编程的首选方案。
1. 核心语法:async函数与await表达式
async/await的使用依赖两个关键字,规则非常明确:
- async关键字:用于声明一个异步函数,该函数的返回值会自动包装成一个Promise对象。
js
async function fn() {
return 1; // 等价于 return Promise.resolve(1)
}
fn().then(result => console.log(result)); // 1
- await关键字:只能在async函数内部使用,用于"等待"一个Promise对象的状态变更。它会暂停当前async函数的执行,直到Promise状态变为fulfilled或rejected,然后继续执行函数并返回结果(或抛出错误)。
js
// 用async/await重构Promise链式调用
async function getGoodsInfo(userId) {
try {
// 等待用户信息请求完成
const user = await requestUserInfo(userId);
// 等待订单请求完成(依赖用户信息)
const order = await requestOrder(user.orderId);
// 等待商品请求完成(依赖订单信息)
const goods = await requestGoods(order.goodsId);
return goods;
} catch (error) {
// 统一捕获所有await的错误
console.error('请求失败:', error);
throw error; // 可向上层抛出错误
}
}
// 调用异步函数
getGoodsInfo(123).then(goods => console.log(goods));
对比之前的回调和Promise链式调用,async/await的优势显而易见:代码完全线性化,逻辑清晰如同步代码,错误处理也通过try/catch统一管理,极大提升了可读性和可维护性。
2. 关键注意点:await的"等待"与并行优化
async/await虽然好用,但如果使用不当,很容易出现"性能问题",这也是面试的高频坑点。比如下面的代码:
js
// 错误示例:不必要的串行等待
async function getMultiData() {
// 两个请求无依赖关系,却串行执行,耗时 = 1s + 1s = 2s
const data1 = await requestData1();
const data2 = await requestData2();
return { data1, data2 };
}
问题在于:requestData1和requestData2之间没有依赖关系,却因为await的串行等待导致总耗时增加。优化方案是先同时触发两个异步操作,再用await等待结果,借助Promise.all实现并行:
js
// 优化:并行执行无依赖的异步操作
async function getMultiData() {
// 同时触发两个请求,无等待
const promise1 = requestData1();
const promise2 = requestData2();
// 等待两个请求都完成,耗时 = 1s(取最长时间)
const data1 = await promise1;
const data2 = await promise2;
return { data1, data2 };
// 或直接用Promise.all:
// const [data1, data2] = await Promise.all([promise1, promise2]);
}
面试必考点:async/await的并行优化。核心原则是"无依赖的异步操作尽量并行执行",通过先创建Promise实例(触发异步操作),再await的方式,或直接结合Promise.all,避免不必要的串行等待。
3. 错误处理:try/catch与Promise.catch的结合
await后面的Promise如果变为rejected状态,会抛出一个异常,因此需要用try/catch捕获。但在实际开发中,也可以结合Promise的catch方法,实现更灵活的错误处理:
js
// 方式1:局部try/catch(捕获指定await的错误)
async function fn1() {
try {
const data = await requestData();
} catch (error) {
console.error('requestData失败:', error);
}
// 其他逻辑不受影响
console.log('继续执行');
}
// 方式2:全局catch(捕获async函数的所有错误)
async function fn2() {
const data = await requestData();
return data;
}
fn2().catch(error => console.error('fn2执行失败:', error));
实际开发中,可根据错误处理的粒度需求选择合适的方式:局部错误用try/catch,全局或跨函数的错误用catch方法。
四、面试高频题:从原理到实战
掌握了基础后,我们结合面试真题,强化对核心知识点的理解。
真题1:Promise的状态变更与then的执行时机
题目:以下代码的输出顺序是什么?
js
console.log('start');
const promise = new Promise((resolve) => {
console.log('promise executor');
resolve('success');
});
promise.then(result => {
console.log(result);
});
console.log('end');
答案:start → promise executor → end → success
解析:Promise构造函数中的executor是同步执行 的,因此会先打印"promise executor";而then方法中的回调是微任务,会在同步代码执行完成后、事件循环的微任务阶段执行,因此"success"会在"end"之后打印。
真题2:async/await与Promise的关系
题目:async/await是Promise的替代方案吗?为什么?
答案:不是替代方案,而是语法糖。原因:
- async函数的返回值本质是Promise对象,await只能等待Promise对象(如果不是,会自动包装成Promise)。
- async/await的底层实现依赖Promise和生成器函数(Generator),它并没有脱离Promise的规范,而是简化了Promise的调用方式。
- 在需要并行执行异步操作、或使用all/race等静态方法时,依然需要结合Promise的API。
真题3:手写Promise简化版(核心逻辑)
题目:实现一个简化版Promise,包含基本的状态管理、resolve/reject和then方法。
js
class MyPromise {
constructor(executor) {
// 初始状态
this.status = 'pending';
// 成功结果
this.value = undefined;
// 失败原因
this.reason = undefined;
// 成功回调队列(支持多个then)
this.onFulfilledCallbacks = [];
// 失败回调队列
this.onRejectedCallbacks = [];
// resolve方法:状态转为成功
const resolve = (value) => {
// 状态不可逆,只有pending才能修改
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = value;
// 执行所有成功回调
this.onFulfilledCallbacks.forEach(cb => cb(value));
}
};
// reject方法:状态转为失败
const reject = (reason) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
// 执行所有失败回调
this.onRejectedCallbacks.forEach(cb => cb(reason));
}
};
// 执行executor,捕获同步错误
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
// then方法:返回新Promise,支持链式调用
then(onFulfilled, onRejected) {
// 兼容then未传回调的情况
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e };
const newPromise = new MyPromise((resolve, reject) => {
// 状态已成功:直接执行回调
if (this.status === 'fulfilled') {
setTimeout(() => { // 模拟微任务异步执行
try {
const result = onFulfilled(this.value);
// 回调结果传递给新Promise的resolve
resolve(result);
} catch (error) {
reject(error);
}
}, 0);
}
// 状态已失败:直接执行回调
if (this.status === 'rejected') {
setTimeout(() => {
try {
const result = onRejected(this.reason);
resolve(result);
} catch (error) {
reject(error);
}
}, 0);
}
// 状态 pending:将回调存入队列
if (this.status === 'pending') {
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const result = onFulfilled(this.value);
resolve(result);
} catch (error) {
reject(error);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const result = onRejected(this.reason);
resolve(result);
} catch (error) {
reject(error);
}
}, 0);
});
}
});
return newPromise;
}
}
解析:简化版Promise的核心是实现"状态不可逆"和"then链式调用"。关键点包括:用队列存储pending状态时的回调、then返回新Promise以支持链式调用、通过setTimeout模拟微任务异步执行、捕获回调中的错误并传递给下一个Promise的reject。
五、总结:从入门到面试通关的核心要点
Promise与async/await的核心是"解决异步编程的流程控制问题",面试考察的不仅是API用法,更是对底层原理和工程实践的理解。最后梳理核心考点:
- Promise的三个状态及不可逆性,executor同步执行、then回调异步执行的特性。
- then的链式调用原理(返回新Promise)和catch的错误捕获范围。
- all/race/allSettled的区别及适用场景,尤其是all的"一错全错"和race的"先到先得"。
- async/await的语法规则,与Promise的依赖关系,以及并行优化的技巧。
- 异步代码的执行顺序(同步→微任务→宏任务),结合Promise和async/await的实际场景分析。