题目
好的,我们进入异步编程的“终极形态”:async/await
。
async/await
是在 ES2017 (ES8) 中引入的,它并不是一个全新的功能,而是建立在 Promise 之上的语法糖 (Syntactic Sugar)。它的目标是让我们能够以一种看似同步、更符合人类直觉的方式来编写和阅读异步代码,从而彻底解决 Promise 链条过长的问题。
练习 05: async/await
- 像写同步代码一样处理异步
在这个练习中,我们不会去修改上一关写的 fetchUserData
函数(因为它返回 Promise,已经很完美了)。我们的任务是改变调用它的方式,用 async/await
来替代 .then()
和 .catch()
。
🎯 学习目标:
- 学会使用
async
关键字来声明一个异步函数。 - 学会使用
await
关键字来“暂停”函数的执行,直到一个 Promise 完成,并获取其结果。 - 理解
await
只能在async
函数内部使用。 - 学会使用标准的
try...catch
语句来捕获await
期间可能发生的错误(即 Promise 的reject
)。
背景知识:
async
function: 在一个函数声明前加上async
关键字,这个函数就变成了一个异步函数。异步函数有一个重要特性:它总是隐式地返回一个 Promise。如果你的函数代码返回了一个具体的值(比如一个对象或字符串),async
函数会自动把它包装在一个fulfilled
状态的 Promise 中返回。await
operator:await
关键字只能用在async
函数内部。它可以“等待”一个 Promise 对象。当代码执行到await somePromise
时,它会暂停当前async
函数的执行,去处理其他任务。直到somePromise
完成(无论是fulfilled
还是rejected
),它才会回来继续执行。- 如果 Promise 成功了,
await
会“解包”这个 Promise,并返回成功的值。 - 如果 Promise 失败了,
await
会抛出一个错误,这个错误可以被try...catch
捕获。
- 如果 Promise 成功了,
🛠️ 任务:
- 创建一个名为
processUserData
的异步函数。 - 在
processUserData
函数内部,使用try...catch
结构来处理可能发生的错误。 - 在
try
代码块中:- 调用我们之前写的
fetchUserData(123)
,并使用await
关键字来获取成功的结果。 - 将获取到的用户数据打印到控制台。
- 调用我们之前写的
- 在
catch
代码块中:- 捕获可能发生的错误,并将其打印到控制台。
- 在主代码中,调用
processUserData
函数来启动整个流程。 - (可选挑战) 尝试在
try
块中也调用一次fetchUserData(-1)
,看看try...catch
是如何处理错误的。
📋 初始代码:
创建新文件 05-async-await.js
。这次你需要从头开始写,但可以把上一关的 fetchUserData
函数复制过来,因为我们依然需要它。
// --- 首先,把上一关的 fetchUserData 函数复制到这里 ---
function fetchUserData(userId) {return new Promise((resolve, reject) => {if (userId <= 0) {reject("Invalid User ID");return;}setTimeout(() => {const user = {id: userId,name: 'John Doe',email: 'john.doe@example.com'};resolve(user);}, 2000);});
}// --- 在这里编写你的新代码 ---/*** 使用 async/await 来处理获取用户数据的流程。*/
async function processUserData() {// 1. 使用 try...catch 来包裹你的异步调用try {// 2. 使用 await 等待 fetchUserData(123) 的结果console.log("正在获取用户数据 (userId: 123)...");const user = "在这里使用 await 获取数据";// 3. 打印成功的结果console.log("成功获取到用户:", user);// --- 可选挑战 ---// console.log("正在尝试获取无效用户 (userId: -1)...");// const invalidUser = await fetchUserData(-1);// console.log("这一行永远不会被打印");} catch (error) {// 4. 捕获并打印错误console.error("在处理过程中发生错误:", error);}
}// --- 最后,调用你的 async 函数来启动整个流程 ---
console.log("程序开始...");
processUserData();
console.log("程序继续执行... (async 函数已调用,但内部代码在等待)");
✅ 预期输出:
程序开始...
正在获取用户数据 (userId: 123)...
程序继续执行... (async 函数已调用,但内部代码在等待)
// ...等待大约2秒...
成功获取到用户: { id: 123, name: 'John Doe', email: 'john.doe@example.com' }
如果你尝试了可选挑战,输出会是:
程序开始...
正在获取用户数据 (userId: 123)...
程序继续执行... (async 函数已调用,但内部代码在等待)
// ...等待大约2秒...
成功获取到用户: { id: 123, name: 'John Doe', email: 'john.doe@example.com' }
正在尝试获取无效用户 (userId: -1)...
在处理过程中发生错误: Invalid User ID
你会发现,代码的逻辑流看起来几乎和同步代码一模一样,非常直观。这就是 async/await
的魔力。
答案
好的,没问题。劳逸结合是高效学习的关键。我们今天就用答案和解析来收尾,明天你再亲手实践,印象会更深刻。
async/await
的美妙之处在于它能让你把异步代码写得像同步代码一样,逻辑清晰,易于阅读。
参考实现 (05-async-await.js
)
// --- 首先,把上一关的 fetchUserData 函数复制到这里 ---
function fetchUserData(userId) {return new Promise((resolve, reject) => {// 增加一个错误检查,让 try...catch 能捕获到它if (userId <= 0) {reject(`Invalid User ID: ${userId}`);return;}// 模拟2秒的网络延迟setTimeout(() => {const user = {id: userId,name: 'John Doe',email: 'john.doe@example.com'};// Promise 成功,并返回 user 对象resolve(user);}, 2000);});
}// --- 在这里编写你的新代码 ---/*** 使用 async/await 来处理获取用户数据的流程。*/
async function processUserData() {console.log("processUserData 函数开始执行...");// 1. 使用标准的 try...catch 语句来包裹可能会出错的异步代码try {// 2. 使用 await 等待 fetchUserData(123) 的 Promise 完成// 代码会在这里“暂停”(非阻塞),直到拿到结果const user = await fetchUserData(123);// 3. 只有在 await 成功后,代码才会继续执行到这里console.log("✅ 成功获取到用户:", user);// --- 可选挑战的实现 ---console.log("---------------------------------");console.log("再次调用,尝试获取一个无效用户...");const invalidUser = await fetchUserData(-1);// 因为上一行会抛出错误,所以这一行永远不会被执行console.log("这一行永远不会被打印", invalidUser);} catch (error) {// 4. 如果 try 块中任何一个 await 的 Promise 被 reject,// 代码会立刻跳转到 catch 块中,并将 reject 的原因赋值给 errorconsole.error("❌ 在处理过程中捕获到错误:", error);}console.log("processUserData 函数执行完毕。");
}// --- 最后,调用你的 async 函数来启动整个流程 ---
console.log("程序开始...");
processUserData();
console.log("程序继续执行... (async 函数已调用,但其内部流程在等待)");
代码解析
-
async function processUserData()
async
关键字告诉 JavaScript 引擎:“processUserData
是一个异步函数,它内部可能含有await
,并且这个函数本身会返回一个 Promise。”
-
const user = await fetchUserData(123);
- 这是
async/await
的核心。 await
做了什么?它告诉processUserData
函数:“请在这里暂停执行,不要往下走了。去等待fetchUserData(123)
这个 Promise 的结果。”- 如果 Promise 成功 (fulfilled):
await
会像拆开礼物包装一样,把 Promise 对象里的成功值(也就是user
对象)取出来,然后把它赋值给左边的const user
变量。之后,函数继续往下执行。 - 如果 Promise 失败 (rejected):
await
会把 Promise 的失败原因(我们代码里的错误信息字符串)当作一个错误抛出 (throw)。
- 这是
-
try...catch
- 这就是
async/await
的优雅之处。因为失败的 Promise 会被await
变成一个抛出的错误,所以我们可以用最传统、最熟悉的try...catch
语句来捕获它! - 当
await fetchUserData(-1)
执行时,Promise 被reject
,await
抛出错误,try
块的执行立刻中断,程序流直接跳转到catch
块,并将错误信息赋值给error
变量。
- 这就是
对比 .then/.catch
看一下两种写法的直观对比:
Promise 链式写法:
fetchUserData(123).then(user => {console.log("成功:", user);}).catch(error => {console.error("失败:", error);});
async/await
写法:
try {const user = await fetchUserData(123);console.log("成功:", user);
} catch (error) {console.error("失败:", error);
}
async/await
的版本看起来就像在写普通的同步代码,从上到下,非常线性,并且用了我们早已熟悉的 try...catch
来处理错误,这大大降低了心智负担。
好的,今天的内容就到这里。async/await
是目前处理异步操作的社区标准和最佳实践,掌握它至关重要。
你先好好休息,我们明天再继续探索 JavaScript 的其他方面!