JavaScript / TypeScript 易踩坑知识点 ------ 异步编程类
一、回调地狱(Callback Hell)
问题说明
多层异步回调嵌套会导致代码难以阅读和维护,形成"箭头型"代码。
踩坑示例
javascript
// ❌ 回调地狱
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
getFinalData(c, function(d) {
console.log(d)
})
})
})
})
修复方案
javascript
// ✅ 方案1:Promise 链式调用
getData()
.then(a => getMoreData(a))
.then(b => getEvenMoreData(b))
.then(c => getFinalData(c))
.then(d => console.log(d))
.catch(err => console.error(err))
// ✅ 方案2:async/await(推荐)
async function fetchData() {
try {
const a = await getData()
const b = await getMoreData(a)
const c = await getEvenMoreData(b)
const d = await getFinalData(c)
console.log(d)
} catch (err) {
console.error(err)
}
}
二、Promise 错误处理缺失
问题说明
Promise 链中忘记添加 .catch() 会导致错误被静默吞掉,难以排查。
踩坑示例
javascript
// ❌ 错误被静默吞掉
fetchData()
.then(data => process(data))
.then(result => save(result))
// 没有 .catch(),任何错误都不会被捕获
// ✅ 正确:链式末尾加 .catch()
fetchData()
.then(data => process(data))
.then(result => save(result))
.catch(err => console.error('处理失败:', err))
// ✅ 更推荐:async/await + try/catch
async function main() {
try {
const data = await fetchData()
const result = await process(data)
await save(result)
} catch (err) {
console.error('处理失败:', err)
}
}
中间步骤遗漏 catch
javascript
// ❌ 第一步的 catch 遗漏了
fetchData()
.catch(err => console.error('获取数据失败')) // 只 catch 了第一步
.then(data => process(data)) // process 报错不会被捕获
.catch(err => console.error('处理失败'))
// ✅ 统一在末尾 catch
fetchData()
.then(data => process(data))
.then(result => save(result))
.catch(err => {
console.error('出错了:', err)
})
三、async/await 中忘记 try/catch
问题说明
async 函数返回的是 Promise,内部的错误如果没有被 try/catch 捕获,会变为未处理的 Promise rejection。
踩坑示例
javascript
// ❌ 忘记 try/catch
async function getUser() {
const res = await fetch('/api/user') // 网络错误会未捕获
const data = await res.json()
return data
}
// 调用方如果没有 catch,错误会静默失败
getUser() // 错误未被处理
// ✅ 正确:内部或外部捕获
// 方案1:内部捕获
async function getUser() {
try {
const res = await fetch('/api/user')
const data = await res.json()
return data
} catch (err) {
console.error('获取用户失败:', err)
throw err // 重新抛出,让调用方处理
}
}
// 方案2:外部捕获(推荐)
async function main() {
try {
const user = await getUser()
console.log(user)
} catch (err) {
console.error('请求失败:', err)
}
}
四、Promise.all 的错误处理
问题说明
Promise.all 中任何一个 Promise 失败,整个 all 会立即 reject,且其他请求会被中断。
踩坑示例
javascript
// ❌ 任何一个失败,其他请求被中断
const [users, posts, comments] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
// ✅ 方案1:用 Promise.allSettled 等待所有完成
const results = await Promise.allSettled([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${i} 成功:`, result.value)
} else {
console.log(`请求 ${i} 失败:`, result.reason)
}
})
// ✅ 方案2:给每个请求单独加 catch
async function safeFetch(url) {
try {
const res = await fetch(url)
return await res.json()
} catch (err) {
return { error: err.message }
}
}
const [users, posts, comments] = await Promise.all([
safeFetch('/api/users'),
safeFetch('/api/posts'),
safeFetch('/api/comments')
])
五、async 函数中的 for 循环
问题说明
在循环中使用 await 时,for...of 和 forEach 的行为完全不同。
踩坑示例
javascript
const urls = ['/api/a', '/api/b', '/api/c']
// ❌ forEach 中的 await 不会等待
urls.forEach(async (url) => {
const data = await fetch(url)
console.log(data)
})
console.log('所有请求完成') // 这行会先执行!
// ✅ 用 for...of 按顺序执行
for (const url of urls) {
const data = await fetch(url)
console.log(data)
}
// ✅ 用 Promise.all 并行执行(不关心顺序时)
const results = await Promise.all(
urls.map(url => fetch(url))
)
六、setTimeout 与 async/await 的混淆
问题说明
setTimeout 返回 undefined,不是 Promise,不能直接 await。
踩坑示例
javascript
// ❌ setTimeout 返回 undefined,await 没有意义
async function wait() {
await setTimeout(() => console.log('2秒后'), 2000)
console.log('这行会立即执行!') // 不会等 2 秒
}
// ✅ 正确:将 setTimeout 包装为 Promise
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function wait() {
await sleep(2000)
console.log('2秒后执行') // ✅ 正确等待
}
七、竞态条件(Race Condition)
问题说明
在快速输入(如搜索框)时,多个异步请求可能以不确定顺序返回,导致旧请求的结果覆盖新请求的结果。
踩坑示例
javascript
// ❌ 竞态条件:用户快速输入 "abc",请求顺序 a->ab->abc,
// 但返回顺序可能是 abc->a->ab,导致显示错误的结果
const search = async (keyword: string) => {
const res = await fetch(`/api/search?q=${keyword}`)
const data = await res.json()
renderResults(data) // ⚠️ 可能是旧请求的结果
}
// ✅ 方案1:用 AbortController 取消旧请求
let controller: AbortController | null = null
const search = async (keyword: string) => {
controller?.abort() // 取消上一次的请求
controller = new AbortController()
try {
const res = await fetch(`/api/search?q=${keyword}`, {
signal: controller.signal
})
const data = await res.json()
renderResults(data)
} catch (err) {
if (err.name === 'AbortError') {
console.log('请求已被取消')
}
}
}
// ✅ 方案2:用序列号防覆盖
let seq = 0
const search = async (keyword: string) => {
const currentSeq = ++seq
const res = await fetch(`/api/search?q=${keyword}`)
const data = await res.json()
if (currentSeq !== seq) return // 有更新的请求,丢弃旧结果
renderResults(data)
}
八、Promise 构造函数反模式
问题说明
用 new Promise 包装已有的 Promise API 是反模式,会破坏错误传播。
踩坑示例
javascript
// ❌ 反模式:不必要的 Promise 包装
function getUsers() {
return new Promise((resolve, reject) => {
fetch('/api/users')
.then(res => res.json())
.then(data => resolve(data))
.catch(err => reject(err))
})
}
// ✅ 直接返回 Promise
function getUsers() {
return fetch('/api/users').then(res => res.json())
}
// ✅ async/await 写法
async function getUsers() {
const res = await fetch('/api/users')
return await res.json()
}
总结对照表
| 坑点 | 错误写法 | 正确写法 |
|---|---|---|
| 回调地狱 | 多层嵌套回调 | async/await |
| Promise 错误吞掉 | 不加 .catch() | 链式末尾加 .catch() |
| async 未捕获 | 不在 try/catch 中调用 | 外部 try/catch 或内部捕获 |
| Promise.all 中断 | 一个失败全部中断 | Promise.allSettled |
| forEach + await | forEach 中用 await | for...of |
| setTimeout + await | await setTimeout() | 包装为 Promise |
| 竞态条件 | 不控制请求顺序 | AbortController 或序列号 |
| Promise 反模式 | new Promise 包装已有 Promise | 直接返回 Promise |