JavaScript 异步控制流:从 Promise 到 async/await 的工程实践

JavaScript 的异步模型从回调地狱走到 async/await,已经相当成熟。但很多开发者还是会踩坑:错误被吞掉、并发控制不当、竞态条件频发。

一、Promise 的基本纪律

1.1 永远处理 reject

// 错误示范
fetch('/api/data').then(res => res.json())

// 正确示范
fetch('/api/data')
  .then(res => res.json())
  .catch(err => {
    console.error('请求失败:', err)
    showErrorMessage(err)
  })

1.2 不要嵌套 Promise

// 回调地狱的 Promise 版本 - 不要这样写
getUser(id).then(user => {
  getOrders(user.id).then(orders => {
    getOrderDetail(orders[0].id).then(detail => {
      // ...
    })
  })
})

// 应该这样写
const user = await getUser(id)
const orders = await getOrders(user.id)
const detail = await getOrderDetail(orders[0].id)

二、并发控制

2.1 Promise.all - 全部成功才成功

const [user, orders, products] = await Promise.all([
  fetchUser(id),
  fetchOrders(id),
  fetchProducts()
])

一个失败,全部失败。适合有强依赖关系的场景。

2.2 Promise.allSettled - 等全部完成

const results = await Promise.allSettled([
  fetch('/api/a'),
  fetch('/api/b'),
  fetch('/api/c')
])

results.forEach((result, i) => {
  if (result.status === 'fulfilled') {
    console.log(`请求 ${i} 成功:`, result.value)
  } else {
    console.log(`请求 ${i} 失败:`, result.reason)
  }
})

适合"部分失败不影响整体"的场景。

2.3 Promise.race - 谁先完成用谁

const response = await Promise.race([
  fetch('/api/fast-server'),
  fetch('/api/backup-server'),
  new Promise((_, reject) => 
    setTimeout(() => reject(new Error('超时')), 5000)
  )
])

三、错误处理的工程化

async function safeFetch(url, options = {}) {
  try {
    const response = await fetch(url, options)
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`)
    }
    return await response.json()
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('请求被取消')
    }
    throw error
  }
}

关键原则:不要在 async 函数里用 try-catch 吞掉错误后不做任何处理。

四、竞态条件处理

用户快速输入搜索时,旧的请求可能比新的请求晚返回:

let latestRequestId = 0

async function search(query) {
  const requestId = ++latestRequestId
  const results = await fetch(`/api/search?q=${query}`)

  // 如果已经有更新的请求,丢弃这次结果
  if (requestId !== latestRequestId) return

  displayResults(results)
}

或者用 AbortController:

let controller = null

async function search(query) {
  if (controller) controller.abort()
  controller = new AbortController()

  const results = await fetch(`/api/search?q=${query}`, {
    signal: controller.signal
  })
  displayResults(results)
}

五、重试机制

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i <= maxRetries; i++) {
    try {
      return await fetch(url, options)
    } catch (err) {
      if (i === maxRetries) throw err
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)))
    }
  }
}

指数退避(1s, 2s, 4s)是重试策略的最佳实践。

六、实用工具函数

// 限制并发数
async function parallelLimit(tasks, limit) {
  const results = []
  const executing = new Set()

  for (const [i, task] of tasks.entries()) {
    const p = task().then(r => { results[i] = r })
    executing.add(p)
    p.then(() => executing.delete(p))

    if (executing.size >= limit) {
      await Promise.race(executing)
    }
  }

  await Promise.all(executing)
  return results
}

异步编程的本质是控制不确定性。掌握这些模式,你的前端代码会更健壮、更可维护。