別再以為 await 到處都一樣:這只是“半個(gè)真相”
大多數(shù)前端以為:有了 await,代碼哪里都能暫停。 抱歉——這話只對了一半。
await 的行為,會因?yàn)?/span>使用場景而劇烈變化。 尤其是:你選的循環(huán)方式,會徹底改變await 的效果。
這是我真希望 6 個(gè)月前就有人告訴我的事,省下我一堆 async/await 的坑。
核心誤解:會“等”的不是所有地方
異步 JS 很“怪”。教程常這么教:函數(shù)前加 async,Promise 前放 await,完事兒。 但在循環(huán) + 異步的組合里:并不是所有循環(huán)都懂得等待。
有的直接無視你的 await; 有的全部并行(即便你不想); 還有的會默默給你整崩。
這里是 await 的“墳場”:forEach
最常見、傷害最大的錯(cuò)誤:**在 forEach 里用 await**。
async function fetchUserProfiles() {
const userIds = [1, 2, 3, 4, 5]
userIds.forEach(async (id) => {
const profile = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
const data = await profile.json()
console.log(`User ${id}:`, data.name)
})
console.log('All done fetching users')
}
fetchUserProfiles()運(yùn)行看看:
- “All done fetching users” 會立刻打印,在任何
fetch完成之前。 - 控制臺順序亂序:2、5、1、3、4……隨緣。
原因:forEach不關(guān)心 Promise。你的回調(diào)是 async,會返回 Promise,但 forEach直接忽略返回值,調(diào)用完就走。 你也沒法 await forEach,因?yàn)樗环祷赜杏玫臇|西。
看起來像對的語法,邏輯也似乎“合理”,但——**forEach 天生不為 Promise 設(shè)計(jì)**。
map 與 Promise 的“隱形坑”
這段代碼在 Review 里很常見,也很迷惑:
async function getAllUserData() {
const userIds = [1, 2, 3]
const userData = await userIds.map(async (id) => {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`)
return response.json()
})
console.log(userData)
}你以為能拿到用戶對象數(shù)組?不,你會得到Promise 數(shù)組。await userIds.map(...)并不會等待里面的異步完成;map 只會收集回調(diào)返回的 Promise。
正確寫法:先收集 Promise,再 Promise.all 一把梭。
async function getAllUserData() {
const userIds = [1, 2, 3]
const userData = await Promise.all(
userIds.map(id =>
fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => res.json())
)
)
console.log(userData)
}不小心的“串行殺性能”
確實(shí)有場景需要嚴(yán)格順序執(zhí)行,這時(shí)用 for…of 很香:
async function processSequentially() {
const ids = [1, 2, 3]
for (const id of ids) {
const result = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
const data = await result.json()
console.log(`Processed ${id}:`, data)
}
console.log('Done')
}這段代碼直觀、可讀、靠譜。 但要小心:純串行意味著每個(gè)都要等上一個(gè)。 如果每次請求 1s,五次就5s。只有在確實(shí)存在依賴/限速/必須有序時(shí)才用串行;否則你就白白慢了 5 倍。
為什么 for…of 會“真的等”
for…of 是語言級的循環(huán),不是數(shù)組方法。 在 for…of 里用 await,整個(gè)循環(huán)會在該處暫停,直到 Promise 解決:
const userIds = [1, 2, 3];
for (const id of userIds) {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`); // 這里暫停
const data = await response.json();
console.log(data); // 等待后才繼續(xù)下一輪
}這與無法暫停的 forEach 本質(zhì)不同。使用場景:強(qiáng)順序、節(jié)流/限流、結(jié)果必須按序。
要速度,就上 Promise.all 并行
當(dāng)任務(wù)互不依賴,且你追求吞吐:
async function processInParallel() {
const ids = [1, 2, 3, 4, 5]
const promises = ids.map(id =>
fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then(res => res.json())
)
const results = await Promise.all(promises)
console.log('All results:', results)
}如果每個(gè)耗時(shí) 1s,并行就是**~1s 完成(不考慮帶寬/服務(wù)端限制)。順序保持與原數(shù)組一致,即便完成順序不同。注意:Promise.all失敗即全部失敗——任何一個(gè) reject,整體直接 reject**,中間結(jié)果拿不到。
更韌性的并行:Promise.allSettled
想要“不管成功失敗都收集”:
async function fetchAllSources() {
const sources = [
'https://api1.com/data',
'https://api2.com/data',
'https://api3.com/data'
]
const promises = sources.map(url =>
fetch(url).then(res => res.json())
)
const results = await Promise.allSettled(promises)
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Source ${index} succeeded:`, result.value)
} else {
console.error(`Source ${index} failed:`, result.reason)
}
})
}allSettled 會等所有 Promise 都 settle(resolve/reject 均可),然后給你完整戰(zhàn)況。
能不用就別用的:reduce + await
在 reduce 里玩 await,讀起來繞、調(diào)試更繞:
const userIds = [1, 2, 3];
async function fetchUsers() {
const users = await userIds.reduce(async (accPromise, id) => {
const acc = await accPromise;
const user = await fetch(`https://pokeapi.co/api/v2/pokemon/${id}`);
return [...acc, user];
}, Promise.resolve([]));
console.log(users);
}
fetchUsers();累加器從第二輪開始就是 Promise,你還得層層 await。 這通常比 for…of 或 Promise.all更難讀也更慢。 除非你確實(shí)在構(gòu)建“步步相依”的序列,否則別選它。
進(jìn)階:可控并發(fā)(批處理)
全串行太慢、全并發(fā)會被限流?中間態(tài):分批并發(fā)。
async function processInBatches(items, batchSize = 5) {
const results = []
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize)
const batchResults = await Promise.all(
batch.map(item =>
fetch(`https://pokeapi.co/api/v2/pokemon/${item}`).then(res => res.json())
)
)
results.push(...batchResults)
}
return results
}每批最多 5 個(gè),請求并發(fā)完成后再進(jìn)下一批:性能優(yōu)于純串行,風(fēng)險(xiǎn)低于無限并發(fā),非常適合第三方 API 的并發(fā)上限。
最后的要點(diǎn)
下次你想在循環(huán)里敲上一個(gè) await,先停半秒,想清楚你要的到底是:
- 強(qiáng)順序(
for…of), - 極速并行(
Promise.all), - 韌性收集(
Promise.allSettled), - 還是限流批處理(分批
Promise.all)。
玩一玩這些模式,歡迎在評論里補(bǔ)充你的最佳實(shí)踐。



























