JavaScript 異步編程的演進
一、 爲什麼需要異步編程?
先說說爲什麼要有異步這回事. JavaScript 是單線程的, 也就是說它一次只能做一件事. 如果所有操作都同步執行, 遇到網絡請求或者文件讀取這種耗時的操作, 頁面就會卡住不動, 用戶體驗直接爆炸.
// 同步代碼的災難現場
const data = fetchDataFromServer(); // 假設這個請求要3秒
console.log(data); // 這行代碼要傻等3秒才能執行
doSomethingElse(); // 更要再等
所以 JavaScript 採用異步非阻塞的方式處理這類操作: 發起請求後不幹等着, 而是繼續執行後面的代碼, 等請求完成後再來處理結果. 這就引出了我們最原始的處理方式——回調函數.
二、回調函數的時代
- 基礎回調:
// 最簡單的回調示例
function fetchData(callback) {
setTimeout(() => {
callback('數據到手啦!');
}, 1000);
}
fetchData((data) => {
console.log(data); // 1秒後輸出"數據到手啦!"
});
- 回調地獄:
當多個異步操作需要順序執行時, 代碼就會變成這樣:
// 經典回調地獄
getUserInfo(userId, function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
getProductInfo(details.productId, function(product) {
console.log('最終結果:', product);
});
});
});
});
這種代碼的問題:
-
難以閱讀和維護 (金字塔形狀)
-
錯誤處理非常麻煩 (要在每個回調裏處理錯誤)
-
無法使用 return 和 throw
-
無法使用 for 循環和 try/catch
- 回調的改進嘗試
2.1 命名函數, 把匿名回調變成命名函數:
getUserInfo(userId, handleUser);
function handleUser(user) {
getOrders(user.id, handleOrders);
}
function handleOrders(orders) {
getOrderDetails(orders[0].id, handleDetails);
}
// ...雖然可讀性好點了,但代碼跳來跳去更暈了
2.2 Async.js 庫, 提供 series/parallel/waterfall 等流程控制:
async.waterfall([
function getUser(callback) {
getUserInfo(userId, callback);
},
function getOrders(user, callback) {
getOrders(user.id, callback);
}
// ...
], function finalHandler(err, result) {
// 最終處理
});
這些方案雖然有一定效果, 但都不夠優雅. 直到 Promise 的出現.
三、Promise
3.1 Promise 直譯就是 "承諾", 它表示一個異步操作的最終完成或失敗. Promise 有三種狀態:
-
pending(進行中)
-
fulfilled(已成功)
-
rejected(已失敗)
一旦狀態改變就不會再變 (要麼成功要麼失敗).
const promise = new Promise((resolve, reject) => {
// 異步操作
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('成功數據');
} else {
reject(new Error('失敗原因'));
}
}, 1000);
});
promise
.then(data => {
console.log('成功:', data);
})
.catch(err => {
console.error('失敗:', err);
});
3.2 Promise 鏈式調用, 這是 Promise 最強大的地方:
// 用Promise改造回調地獄
getUserInfo(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => getProductInfo(details.productId))
.then(product => {
console.log('最終結果:', product);
})
.catch(err => {
console.error('出錯啦:', err);
});
對比之前的回調地獄, 這種鏈式調用:
-
扁平化, 可讀性極佳
-
錯誤處理統一 (一個 catch 處理所有錯誤)
-
可以使用 return 傳遞數據
-
可以在 then 裏 throw error
3.3 Promise 的靜態方法:
Promise.all: 等所有 Promise 都成功.
Promise.all([promise1, promise2, promise3])
.then(([result1, result2, result3]) => {
// 所有都成功纔會到這裏
})
.catch(err => {
// 任意一個失敗就到這裏
});
Promise.race:競速, 第一個完成的 Promise 決定結果.
Promise.race([fetchFromA(), fetchFromB()])
.then(firstResult => {
// 使用最先返回的結果
});
**Promise.allSettled: **等所有 Promise 都完成 (無論成功失敗).
Promise.allSettled([promise1, promise2])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.log('失敗:', result.reason);
}
});
});
3.4 Promise 的侷限性
雖然 Promise 很棒, 但還是有不足:
-
無法取消 Promise.
-
錯誤處理雖然比回調好, 但還是要用 catch.
-
多個 Promise 之間的數據共享不太方便.
-
調試時堆棧信息不友好.
四、aysnc/await
async/await 是現代 JavaScript 異步編程的終極解決方案, 它讓異步代碼看起來和同步代碼一樣直觀, 用起來更簡單:
async function fetchData() {
try {
const user = await getUserInfo(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
console.log(details);
} catch (err) {
console.error(err);
}
}
這代碼看起來完全是同步的風格, 但實際上是異步執行的!
4.1 async/await 函數的特徵
總是返回 Promise:
-
如果返回非 Promise 值, 會自動包裝成 Promise.resolve(值)
-
如果拋出異常, 返回 Promise.reject(異常)
await 只能在 async 函數中使用:
-
普通函數中使用會報語法錯誤
-
現代瀏覽器和 Node.js 的模塊系統中支持頂層 await
4.2 await 解析
4.2.1 await 的行爲
async function example() {
const result = await somePromise;
console.log(result);
}
這段代碼實際上做了以下幾件事:
-
暫停 example 函數的執行
-
等待 somePromise 完成
-
如果 Promise 成功, 恢復執行並將結果賦值給 result
-
如果 Promise 失敗, 拋出拒絕原因 (可以用 try/catch 捕獲)
4.2.2 await 不一定要等 Promise
雖然設計初衷是處理 Promise, 但 await 可以等待任何值:
async function foo() {
const a = await 42; // 等價於 await Promise.resolve(42)
const b = await someFunction(); // 如果someFunction返回非Promise,也會自動包裝
}
4.2.3 錯誤處理機制
await 的錯誤處理非常符合直覺, 可以用傳統的 try/catch:
async function fetchData() {
try {
const data = await fetch('/api');
const json = await data.json();
console.log(json);
} catch (err) {
console.error('請求失敗:', err);
}
}
對比 Promise 的. catch(), 這種寫法更接近同步代碼的思維模式.
五、常見使用方式
5.1 順序執行:
async function sequential() {
const user = await getUser();
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
return comments;
}
5.2 並行執行:
async function parallel() {
const [user, posts] = await Promise.all([
getUser(),
getPosts()
]);
return { user, posts };
}
5.3 循環中的 await:
// 順序處理數組
async function processArray(array) {
for (const item of array) {
await processItem(item);
}
}
// 並行處理數組
async function processArrayParallel(array) {
await Promise.all(array.map(item => processItem(item)));
}
5.4 await 可以等待任何 thenable 對象:
async function example() {
const result = await {
then(resolve) {
setTimeout(() => resolve('done'), 1000);
}
};
console.log(result); // 1秒後輸出'done'
}
5.5 混合使用 Promise 和 async/await:
async function getUserPosts(userId) {
return fetchUser(userId) // 返回Promise
.then(user => fetchPosts(user.id))
.then(posts => {
return asyncFunction(posts); // 在then中調用async函數
});
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/u1TboKTShXb3v_Zy2j9OzQ