在JavaScript的异步编程场景中,传统的回调函数写法容易导致回调地狱,Promise链式调用虽然解决了嵌套问题,但过多的then方法也会让代码逻辑显得分散。async函数是处理异步操作的更优方案,它基于Promise实现,能让异步代码的编写风格更接近同步代码,大幅提升代码的可读性和可维护性。

传统异步写法的不足
早期处理异步操作通常使用回调函数,例如读取文件后执行后续逻辑,多层嵌套时会出现如下代码:
// 回调地狱示例
function readFileStep1(callback) {
setTimeout(() => {
console.log('读取第一步数据');
callback();
}, 1000);
}
function readFileStep2(callback) {
setTimeout(() => {
console.log('读取第二步数据');
callback();
}, 1000);
}
function readFileStep3() {
setTimeout(() => {
console.log('读取第三步数据');
}, 1000);
}
// 嵌套调用
readFileStep1(() => {
readFileStep2(() => {
readFileStep3();
});
});
这种写法嵌套层级多了之后,代码缩进会越来越深,逻辑跳转不直观,排查问题也比较困难。后来Promise出现后,我们可以用链式调用优化:
// Promise链式调用示例
function readFileStep1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('读取第一步数据');
resolve();
}, 1000);
});
}
function readFileStep2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('读取第二步数据');
resolve();
}, 1000);
});
}
function readFileStep3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('读取第三步数据');
resolve();
}, 1000);
});
}
// 链式调用
readFileStep1()
.then(() => readFileStep2())
.then(() => readFileStep3());
Promise链式调用虽然解决了嵌套问题,但如果步骤较多,仍然会有很多then方法,逻辑还是不够直观,而且如果需要在多个步骤之间传递数据,还要额外处理参数传递。
async函数的基本用法
async函数是声明异步函数的关键字,被async修饰的函数会返回一个Promise对象,函数内部的await关键字可以暂停函数执行,等待后面的Promise完成后再继续往下走。我们可以用async函数改写上面的示例:
// async函数改写示例
function readFileStep1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('读取第一步数据');
resolve('step1_result');
}, 1000);
});
}
function readFileStep2(preResult) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('读取第二步数据,上一步结果:' + preResult);
resolve('step2_result');
}, 1000);
});
}
function readFileStep3(preResult) {
return new Promise((resolve) => {
setTimeout(() => {
console.log('读取第三步数据,上一步结果:' + preResult);
resolve('step3_result');
}, 1000);
});
}
// 定义async函数
async function runSteps() {
const result1 = await readFileStep1();
const result2 = await readFileStep2(result1);
const result3 = await readFileStep3(result2);
console.log('所有步骤完成,最终结果:' + result3);
}
// 执行函数
runSteps();
可以看到,改写后的代码逻辑和同步代码几乎一致,从上到下依次执行,每一步的结果可以直接赋值给变量,不需要额外的参数传递逻辑,可读性大幅提升。
async函数的错误处理优化
传统Promise的错误处理需要用catch方法,而async函数中可以用try...catch语法捕获错误,更符合同步代码的错误处理习惯:
// 带错误处理的async函数示例
function requestData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 模拟随机失败
if (Math.random() > 0.5) {
resolve('请求成功的数据');
} else {
reject(new Error('请求失败'));
}
}, 1000);
});
}
async function fetchData() {
try {
const data = await requestData();
console.log('获取数据成功:' + data);
} catch (error) {
console.log('获取数据失败:' + error.message);
}
}
fetchData();
如果不需要针对单个await做错误处理,也可以在调用async函数的时候用catch捕获整个函数的错误:
async function fetchData() {
const data = await requestData();
console.log('获取数据成功:' + data);
}
fetchData().catch(error => {
console.log('捕获到错误:' + error.message);
});
并发场景的优化技巧
如果多个异步操作之间没有依赖关系,不需要按顺序执行,用await逐个等待会降低执行效率,这时候可以用Promise.all来并发执行,再配合await等待所有结果:
// 并发执行优化示例
function requestA() {
return new Promise(resolve => {
setTimeout(() => {
console.log('请求A完成');
resolve('A的结果');
}, 2000);
});
}
function requestB() {
return new Promise(resolve => {
setTimeout(() => {
console.log('请求B完成');
resolve('B的结果');
}, 1500);
});
}
function requestC() {
return new Promise(resolve => {
setTimeout(() => {
console.log('请求C完成');
resolve('C的结果');
}, 1000);
});
}
async function runConcurrent() {
console.log('开始并发请求');
// 同时发起三个请求,等待所有完成
const [resultA, resultB, resultC] = await Promise.all([
requestA(),
requestB(),
requestC()
]);
console.log('所有请求完成,结果:', resultA, resultB, resultC);
}
runConcurrent();
上面的例子中,三个请求是同时发起的,总耗时是最慢的那个请求的耗时,而不是三个请求的耗时相加,能大幅提升执行效率。如果不需要等待所有结果,只需要拿到第一个完成的结果,可以用Promise.race配合await。
注意事项
- await关键字只能用在async函数内部,用在普通函数里会报语法错误。
- await后面如果不是Promise对象,会自动被转换成resolved状态的Promise,所以可以直接await一个非Promise的值。
- 如果await的Promise被reject,且没有被try...catch捕获,会导致整个async函数返回的Promise被reject,需要在合适的层级做错误处理。
- 不要在循环里直接用await,除非是确实需要按顺序执行每个循环的异步操作,否则优先用Promise.all处理并发。
总结
async函数通过await关键字让异步代码的编写逻辑更接近同步代码,解决了回调地狱和Promise链式调用逻辑分散的问题,同时配合try...catch和Promise的并发方法,能覆盖绝大多数异步场景的优化需求。在实际开发中,合理运用async函数可以让异步代码更简洁、更易维护,是提升JavaScript代码质量的重要手段。