JavaScript异步编程的终极进化:Async/Await 完全指南
async/await 是 ES2017(2017)纳入标准的语法糖,基于 Promise,解决了 Promise 链式调用的繁琐,让异步代码像同步代码;
一、async/await 的发展史:为什么会出现?
要理解 async/await,得先回顾它的 “前辈” 们 ——async/await 不是凭空出现的,而是为了解决 Promise 的 “小痛点”,是 JS 异步编程的终极语法糖。
1. 异步编程的 “进化史”(铺垫)
JS 异步编程的发展,核心是 “让异步代码更像同步代码”,一步步解决 “写起来别扭” 的问题:
- 阶段 1:回调函数(ES5 及以前):解决了异步执行的问题,但多层嵌套形成 “回调地狱”,代码臃肿;
- 阶段 2:Promise(ES2015/ES6,2015 年):解决了回调地狱,把嵌套改成链式调用,但链式调用多了(比如 5-6 个异步操作),then依然会串联成 “长链条”,逻辑不够直观;
- 阶段 3:Generator 函数(ES2015/ES6,过渡方案):可以暂停 / 恢复函数执行,能模拟 “同步写异步”,但语法繁琐(需要手动调用
next()),还需要第三方库(如 co)封装,普通人难用; - 阶段 4:async/await(ES2017/ES8,2017 年):基于
Promise和Generator,是官方推出的 “语法糖”—— 既保留了 Promise 的异步特性,又能**像写同步代码一样写异步逻辑**,成为现在异步编程的主流。
2. 各阶段具体代码示例
阶段一:回调函数时代(Callback Hell)
getData(function(data1) {
process(data1, function(data2) {
save(data2, function(data3) {
display(data3, function() {
// 更多嵌套...
console.log("完成了,但代码已经看不懂了!");
});
});
});
});
问题:代码横向发展,形成"金字塔",难以维护和调试。
阶段二:Promise时代(ES6,2015年)
getData()
.then(process)
.then(save)
.then(display)
.then(() => console.log("完成"))
.catch(error => console.error("出错:", error));
改进:链式调用,纵向发展,错误处理统一。
阶段三:Generator函数时代(过渡方案)
// 2015年:Generator + Promise
function* asyncGenerator() {
const data1 = yield getData();
const data2 = yield process(data1);
const data3 = yield save(data2);
yield display(data3);
console.log("完成");
}
// 需要外部执行器
function run(generator) {
const iterator = generator();
function handle(result) {
if (result.done) return;
result.value.then(data => {
handle(iterator.next(data));
});
}
handle(iterator.next());
}
问题:需要外部执行器,语法复杂,不直观。
阶段四:Async/Await时代(ES8,2017年)
// 2017年:Async/Await
async function handleData() {
try {
const data1 = await getData();
const data2 = await process(data1);
const data3 = await save(data2);
await display(data3);
console.log("完成");
} catch (error) {
console.error("出错:", error);
}
}
革命性改进:代码看起来像同步代码,但实际上是异步执行!
3. 核心本质
async/await 不是替代 Promise,而是基于 Promise 的语法糖—— 所有 async/await 能实现的功能,用 Promise 都能实现,只是 async/await 写起来更简单、更易读。
二、async/await 的核心用法
1. 基本语法:async + await
(1)async:声明 “异步函数”
- 用
async修饰的函数,会变成 “异步函数”; - 异步函数的返回值会自动包装成 Promise(哪怕你返回的是普通值)。
// 在函数前加上 async,这个函数就变成了异步函数
async function normalFunction() {
return "Hello Async"; // 自动包装成Promise
}
// 等价于:
function normalFunction() {
return Promise.resolve("Hello Async");
}
// 使用
normalFunction().then(console.log); // "Hello Async"
(2)await:等待 Promise 完成
await只能用在async 函数内部(这是新手最容易踩的坑);- await后面必须跟一个Promise 对象(如果不是,会直接返回该值);
- await会 “暂停” 函数执行,直到 Promise 完成(成功 / 失败),再继续执行后面的代码 —— 但这个 “暂停” 是非阻塞的(不会卡主线程)。
async function fetchUser() {
// await 会暂停函数执行,等待Promise完成
const response = await fetch('https://api.example.com/user');
const user = await response.json();
return user;
}
// await 只能在 async 函数内部使用
// 下面的代码会报错:
// const data = await fetchUser(); // ❌ 错误
💡 对比 Promise:原来的.then链式调用,现在直接用await拿到结果,代码和同步逻辑完全一致!
(3)基本用法示例
// 模拟异步函数
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function example() {
console.log("开始");
await delay(1000); // 等待1秒
console.log("1秒后");
await delay(2000); // 再等待2秒
console.log("再过2秒后");
return "任务完成";
}
// 调用
example().then(result => console.log(result));
// 输出:
// 开始
// 1秒后
// 再过2秒后
// 任务完成
2. 错误处理:try/catch(核心)
Promise 用.catch处理错误,async/await 用try/catch捕获错误(因为 await 会 “解包” Promise 的失败状态,必须手动捕获)。
示例 1:捕获单个异步操作的错误
async function getScoreAsync() {
try {
// 成功:执行try里的代码,失败:跳转到catch
const score = await getScore();
console.log("分数:", score);
} catch (error) {
// 捕获Promise的reject错误
console.log("出错了:", error);
}
}
// 测试:如果getScore返回reject(比如分数50),会执行catch
getScoreAsync();
示例 2:捕获多个异步操作的错误
如果有多个 await,一个 try/catch 可以捕获所有错误:
// 模拟两个异步请求
function getUser() {
return new Promise((resolve) => setTimeout(() => resolve({ id: 1 }), 500));
}
function getCourse(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
userId === 1 ? resolve({ name: "语文" }) : reject("用户不存在");
}, 500);
});
}
// 用async/await调用,统一捕获错误
async function getInfo() {
try {
const user = await getUser(); // 第一个异步操作
const course = await getCourse(user.id); // 第二个异步操作
console.log("用户课程:", course); // 输出:{ name: "语文" }
} catch (error) {
console.log("错误:", error); // 任何一个异步失败,都会走到这里
}
}
getInfo();
3. 实战案例:替代 Promise 链式调用(解决回调地狱)
回顾之前 “登录→查课程→查成绩” 的案例,用 async/await 改写后,代码更简洁:
// 封装3个异步请求(返回Promise)
function login() {
return new Promise(resolve => setTimeout(() => resolve({ id: 1 }), 500));
}
function getCourse(userId) {
return new Promise(resolve => setTimeout(() => resolve({ id: 10 }), 500));
}
function getScore(courseId) {
return new Promise(resolve => setTimeout(() => resolve({ score: 90 }), 500));
}
// async/await版本:同步写法,异步执行
async function getStudentInfo() {
try {
const user = await login(); // 登录
const course = await getCourse(user.id); // 查课程
const score = await getScore(course.id); // 查成绩
console.log("最终成绩:", score); // 输出:{ score: 90 }
} catch (error) {
console.log("出错了:", error);
}
}
getStudentInfo();
对比 Promise 链式调用:没有任何.then,代码和同步逻辑完全一致,新手也能一眼看懂执行顺序。
4. 进阶用法:并发执行(避免不必要的等待)
新手容易犯的错:用 await 串行执行所有异步操作,导致耗时过长。比如:
// 错误示例:串行执行(总耗时=1s+1s=2s)
async function badDemo() {
const res1 = await new Promise(resolve => setTimeout(() => resolve(1), 1000));
const res2 = await new Promise(resolve => setTimeout(() => resolve(2), 1000));
console.log(res1, res2); // // 总时间 = 2个请求时间之和=2s
}
如果两个异步操作互不依赖(比如同时查两个独立的接口),应该用Promise.all实现并发:
// 正确示例:并发执行(总耗时=1s)
async function goodDemo() {
// 1. 先创建两个Promise对象(不要先await)
const promise1 = new Promise(resolve => setTimeout(() => resolve(1), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve(2), 1000));
// 2. 用Promise.all并发执行,await等待所有完成
const [res1, res2] = await Promise.all([promise1, promise2]);
console.log(res1, res2); // 总耗时1秒
}
💡 核心原则:
- 异步操作有依赖(比如查成绩依赖课程 ID):用
await串行执行; - 异步操作无依赖:用
Promise.all+await并发执行,提升效率。
5. 循环中的 await:避免踩坑
如果在循环中用 await,默认是串行执行,比如:
// 串行执行:总耗时3秒(1+1+1)
async function loopDemo() {
const list = [1, 2, 3];
for (const item of list) {
await new Promise(resolve => setTimeout(() => {
console.log(item);
resolve();
}, 1000));
}
}
注意:forEach 中的 async/await 不会按预期工作(❌ 错误)
如果想让循环中的异步操作并发执行,先收集所有 Promise,再用Promise.all:
// map + Promise.all(并行) 并发执行:总耗时1秒
async function loopDemo() {
const list = [1, 2, 3];
// 1. 收集所有Promise
const promises = list.map(item =>
new Promise(resolve => setTimeout(() => {
console.log(item);
resolve();
}, 1000))
);
// 2. 并发执行
await Promise.all(promises);
}
三、async/await 的常见坑与注意事项
- await 只能在 async 函数中使用:如果在普通函数里用 await,会直接报错(比如在全局作用域、普通 for 循环回调里);
✅ 解决:要么把函数改成 async,要么用 IIFE(立即执行函数)包裹:
// 全局作用域使用await(ES模块环境)
(async () => {
const score = await getScore();
console.log(score);
})();
-
不要滥用 await:所有异步操作都用 await 串行执行,会导致性能极低(比如 10 个独立接口,本可以 1 秒完成,结果用了 10 秒);
✅ 解决:无依赖的异步操作,用Promise.all并发执行。 -
错误捕获要完整:如果漏写 try/catch,await 的 Promise 失败时,会抛出 “未捕获的错误”,导致程序崩溃;
✅ 解决:要么用 try/catch,要么在 await 后面加.catch:// 单个异步操作的简化错误捕获 const score = await getScore().catch(error => console.log(error));
四、总结:核心要点回顾
发展史:async/await 是 ES2017(2017)纳入标准的语法糖,基于 Promise,解决了 Promise 链式调用的繁琐,让异步代码像同步代码;
核心语法:
- async修饰函数,使其返回 Promise;
- await只能在 async 函数内使用,等待 Promise 完成并返回结果;
- 错误处理用try/catch(替代 Promise 的.catch)。
使用原则:
- 有依赖的异步操作:await 串行执行;
- 无依赖的异步操作:Promise.all+await 并发执行;
- 必加错误捕获,避免程序崩溃。
async/await 是目前 JS 异步编程的 “最优解”,掌握它后,不管是前端调接口、Node.js 写后端,处理异步逻辑都会变得极其简单~
更多推荐



所有评论(0)