題目
好的,我們進入異步編程的“終極形態”: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 的其他方面!