JS 中async/await關鍵詞的使用解析
在 JavaScript 異步編程領域,async/await是 ES2017 推出的革命性語法糖,它基於 Promise 實現,核心作用是用同步代碼的寫法實現異步邏輯,徹底解決了傳統回調函數(回調地獄)和 Promise 鏈式調用(.then()嵌套)的可讀性問題。async用於標記函數為異步函數,await用於暫停異步函數執行、等待 Promise 完成並獲取其結果。掌握async/await是現代 JS 異步開發的必備技能,尤其在處理接口請求、文件讀寫、定時任務等異步場景時,能讓代碼邏輯更清晰、更易維護。
一、async/await 的基礎用法
1. 核心語法規則
-
async:修飾函數(普通函數 / 箭頭函數),使其成為異步函數,異步函數的返回值會自動包裝為 Promise 對象(即使返回基本類型,也會被Promise.resolve()包裹);
-
await:只能在async函數內部使用,作用於 Promise 對象時,會暫停函數執行,直到 Promise 狀態變為fulfilled(成功)或rejected(失敗),並返回 Promise 的結果(成功值 / 拋出錯誤)。
2. 基礎示例:同步寫法實現異步
// 模擬異步接口請求(返回Promise)
function fetchUser() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: "張三", age: 28 });
}, 1000);
});
}
// 異步函數:用async標記,內部用await等待Promise
async function getUserInfo() {
console.log("開始請求用户數據");
// 暫停執行,等待fetchUser的Promise完成,獲取結果
const user = await fetchUser();
console.log("請求完成:", user);
return user; // 返回值自動包裝為Promise
}
// 調用異步函數(返回Promise,可通過.then()獲取結果)
getUserInfo().then(res => {
console.log("最終結果:", res);
});
// 輸出順序(間隔1秒):
// 開始請求用户數據
// 請求完成: { name: '張三', age: 28 }
// 最終結果: { name: '張三', age: 28 }
3. 異步函數的返回值特性
// 1. 返回基本類型:自動包裝為Promise
async function returnBasic() {
return "hello async";
}
console.log(returnBasic()); // 輸出:Promise { 'hello async' }
returnBasic().then(res => console.log(res)); // 輸出:hello async
// 2. 返回Promise:直接返回該Promise(不二次包裝)
async function returnPromise() {
return new Promise((resolve) => resolve(123));
}
returnPromise().then(res => console.log(res)); // 輸出:123
// 3. 無返回值:默認返回Promise { undefined }
async function noReturn() {}
noReturn().then(res => console.log(res)); // 輸出:undefined
二、await 的核心特性
1. 等待 Promise 成功並獲取結果
await後跟 Promise 對象時,會 “解包” Promise 的成功值,直接返回給變量,無需.then()鏈式調用。
// 模擬多個異步請求
function fetchUser() { return Promise.resolve({ id: 1 }); }
function fetchUserPosts(userId) {
return Promise.resolve([{ postId: 101, userId }]);
}
async function getUserPosts() {
// 第一步:獲取用户信息(等待Promise完成)
const user = await fetchUser();
// 第二步:用用户ID獲取帖子(依賴第一步結果,同步寫法)
const posts = await fetchUserPosts(user.id);
console.log("用户帖子:", posts);
return posts;
}
getUserPosts();
// 輸出:用户帖子: [ { postId: 101, userId: 1 } ]
2. 處理 Promise 失敗(錯誤捕獲)
await後的 Promise 若變為rejected狀態,會直接拋出錯誤,需通過try/catch捕獲(替代 Promise 的.catch()),這是async/await處理錯誤的標準方式。
// 模擬失敗的異步請求
function fetchFailed() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("請求失敗:網絡異常"));
}, 1000);
});
}
async function handleError() {
try {
// 等待失敗的Promise,會拋出錯誤
const result = await fetchFailed();
console.log("請求成功:", result);
} catch (error) {
// 捕獲錯誤並處理
console.error("捕獲錯誤:", error.message);
}
}
handleError();
// 輸出:捕獲錯誤: 請求失敗:網絡異常
3. 並行執行多個異步任務
默認情況下,await是串行執行(前一個完成才執行下一個),若多個異步任務無依賴關係,需用Promise.all()實現並行,提升執行效率。
// 模擬兩個獨立的異步請求
function fetchA() { return new Promise(resolve => setTimeout(() => resolve("A"), 1000)); }
function fetchB() { return new Promise(resolve => setTimeout(() => resolve("B"), 1000)); }
// 串行執行(總耗時≈2秒)
async function serialExecute() {
console.time("serial");
const a = await fetchA();
const b = await fetchB();
console.log("串行結果:", a, b);
console.timeEnd("serial"); // 輸出:serial: 2000+ms
}
// 並行執行(總耗時≈1秒)
async function parallelExecute() {
console.time("parallel");
// 先創建Promise實例(啓動異步任務)
const promiseA = fetchA();
const promiseB = fetchB();
// 同時等待多個Promise完成
const a = await promiseA;
const b = await promiseB;
// 或直接用Promise.all:const [a, b] = await Promise.all([fetchA(), fetchB()]);
console.log("並行結果:", a, b);
console.timeEnd("parallel"); // 輸出:parallel: 1000+ms
}
serialExecute();
parallelExecute();
三、async/await 的核心應用場景
1. 處理 AJAX / 接口請求(替代.then ())
這是async/await最常用的場景,結合fetch/axios等請求庫,讓異步請求邏輯更直觀。
// 基於fetch的接口請求(async/await版)
async function fetchApiData(url) {
try {
// 等待響應
const response = await fetch(url);
// 檢查HTTP狀態碼(fetch僅在網絡錯誤時reject,狀態碼非2xx需手動判斷)
if (!response.ok) {
throw new Error(`HTTP錯誤:${response.status}`);
}
// 等待JSON解析
const data = await response.json();
return data;
} catch (error) {
console.error("接口請求失敗:", error.message);
// 拋出錯誤,讓調用方處理
throw error;
}
}
// 調用
fetchApiData("https://api.example.com/user")
.then(data => console.log("接口數據:", data))
.catch(err => console.log("最終錯誤處理:", err));
2. 異步遍歷(for...of + await)
forEach循環中無法直接使用await(forEach 是同步遍歷,會忽略 await),需改用for...of循環實現異步遍歷。
// 模擬批量異步請求
function fetchItem(id) {
return new Promise(resolve => {
setTimeout(() => resolve(`商品${id}`), 500);
});
}
// 異步遍歷數組
async function batchFetch(ids) {
const results = [];
// for...of支持await,串行遍歷
for (const id of ids) {
const item = await fetchItem(id);
results.push(item);
console.log("獲取到:", item);
}
return results;
}
batchFetch([1, 2, 3]);
// 輸出(間隔500ms):
// 獲取到: 商品1
// 獲取到: 商品2
// 獲取到: 商品3
3. 結合 Promise.race 實現超時控制
Promise.race()可讓多個 Promise 競爭,結合async/await能輕鬆實現異步任務的超時控制。
// 封裝超時函數
function timeout(delay) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`請求超時(${delay}ms)`));
}, delay);
});
}
// 帶超時控制的異步請求
async function fetchWithTimeout(url, delay = 3000) {
try {
// 競爭:請求和超時誰先完成,就執行誰
const result = await Promise.race([
fetch(url),
timeout(delay)
]);
return result.json();
} catch (error) {
console.error("請求超時/失敗:", error.message);
return null;
}
}
// 調用(若接口3秒內未返回,觸發超時)
fetchWithTimeout("https://api.example.com/slow-data", 3000);
4. 頂層 await(ES2022)
ES2022 允許在模塊頂層使用await(無需包裹在 async 函數中),適用於模塊加載時的異步初始化(如加載配置、預請求數據)。
// 模塊文件(如config.js)
// 頂層await:加載遠程配置
const config = await fetch("https://api.example.com/config").then(res => res.json());
export default config;
// 主文件
import config from "./config.js";
console.log("配置加載完成:", config);
注意:頂層 await 僅在 ES 模塊(type="module"的腳本 /import/export模塊)中生效,普通腳本中仍需包裹在 async 函數中。
四、async/await 的注意事項
1. await 只能在 async 函數中使用
普通函數中使用await會直接拋出語法錯誤,即使是回調函數,也需確保其所在函數是 async 函數。
// 錯誤示例:普通函數中使用await
function normalFunc() {
const res = await fetchUser(); // 報錯:Uncaught SyntaxError: await is only valid in async functions
}
// 正確示例:回調函數標記為async
setTimeout(async () => {
const res = await fetchUser();
console.log(res);
}, 1000);
2. 未捕獲的錯誤會導致 Promise 拒絕
若async函數內部的await拋出錯誤且未用try/catch捕獲,異步函數返回的 Promise 會變為rejected狀態,需在調用時用.catch()捕獲,否則會觸發未捕獲的 Promise 錯誤。
async function uncaughtError() {
await Promise.reject(new Error("未捕獲的錯誤"));
}
// 錯誤:未捕獲的Promise錯誤
// uncaughtError();
// 正確:調用時捕獲
uncaughtError().catch(err => console.error("捕獲錯誤:", err.message));
3. 避免濫用 await(串行阻塞並行)
若多個異步任務無依賴關係,不要逐個await(串行執行),應使用Promise.all()並行執行,否則會浪費性能。
// 不推薦:串行執行(總耗時≈2秒)
async function badParallel() {
const a = await fetchA();
const b = await fetchB();
return [a, b];
}
// 推薦:並行執行(總耗時≈1秒)
async function goodParallel() {
const [a, b] = await Promise.all([fetchA(), fetchB()]);
return [a, b];
}
4. async 函數中的 return 會包裝為 Promise
即使async函數內部用return返回值,調用時也需用.then()或await獲取,直接賦值會得到 Promise 對象。
async function getNum() {
return 100;
}
const num = getNum();
console.log(num); // 輸出:Promise { 100 }
console.log(await getNum()); // 輸出:100(需在async函數中)
五、async/await vs Promise vs 回調函數
| 特性 | 回調函數 | Promise | async/await |
|---|---|---|---|
| 代碼可讀性 | 差(回調地獄) | 中(鏈式調用) | 優(同步寫法) |
| 錯誤處理 | 嵌套 try/catch | .catch () 鏈式捕獲 | try/catch(同步方式) |
| 並行執行 | 需手動控制 | Promise.all/race | 結合 Promise.all/race |
| 調試難度 | 高(棧混亂) | 中(鏈式棧) | 低(同步棧) |
| 兼容性 | 全兼容 | ES6+ | ES2017+/ 轉譯後兼容 |
總結
-
async標記函數為異步函數,其返回值自動包裝為 Promise;await僅在 async 函數內生效,用於暫停執行並獲取 Promise 結果。
-
async/await的錯誤處理依賴try/catch,未捕獲的錯誤會導致 Promise 拒絕,需在調用時用.catch()兜底。
-
無依賴的異步任務需用Promise.all()並行執行,避免串行阻塞;結合Promise.race()可實現超時控制。
-
async/await是 Promise 的語法糖,完全兼容 Promise 生態,是現代 JS 異步編程的首選方式,大幅提升代碼可讀性和可維護性。