前端开发

深入理解 JavaScript 异步编程

从回调到 Promise 再到 async/await,彻底搞懂 JavaScript 异步编程的演进与最佳实践。

站长 15 分钟阅读

深入理解 JavaScript 异步编程

JavaScript 是单线程语言,但通过事件循环机制实现了异步编程。本文将全面讲解异步编程的演进历程。

1. 为什么需要异步

JavaScript 在浏览器中运行在一个单线程上。如果所有操作都是同步的,一个耗时操作就会阻塞整个页面:

// 同步:阻塞
const data = syncFetch('/api/users')  // 网络请求耗时 2 秒
console.log('这行代码要等 2 秒后才执行')

// 异步:非阻塞
asyncFetch('/api/users').then(data => {
  console.log('请求完成', data)
})
console.log('这行代码立即执行')

2. 回调函数(Callback)

最早的异步方案:

function fetchData(url, callback) {
  setTimeout(() => {
    callback('数据')
  }, 1000)
}

fetchData('/api', (data) => {
  console.log(data)
})

回调地狱

// ❌ 回调地狱
fetchUser(id, (user) => {
  fetchOrders(user.id, (orders) => {
    fetchOrderDetail(orders[0].id, (detail) => {
      fetchProducts(detail.productId, (products) => {
        console.log(products)
      })
    })
  })
})

3. Promise

Promise 解决了回调地狱问题,提供了链式调用:

fetchUser(id)
  .then(user => fetchOrders(user.id))
  .then(orders => fetchOrderDetail(orders[0].id))
  .then(detail => fetchProducts(detail.productId))
  .then(products => console.log(products))
  .catch(err => console.error('出错了:', err))

Promise 的三种状态

  • pending — 初始状态,既不是成功也不是失败
  • fulfilled — 操作成功
  • rejected — 操作失败

常用 API

// 并行执行所有 Promise
Promise.all([p1, p2, p3])
  .then(results => console.log(results))

// 返回第一个完成的(无论成功或失败)
Promise.race([p1, p2])
  .then(result => console.log(result))

// 等待所有完成(无论成功或失败)
Promise.allSettled([p1, p2, p3])
  .then(results => {
    results.forEach(r => {
      if (r.status === 'fulfilled') {
        console.log('成功:', r.value)
      } else {
        console.log('失败:', r.reason)
      }
    })
  })

4. async/await

async/await 是 Promise 的语法糖,让异步代码看起来像同步代码:

async function getData() {
  try {
    const user = await fetchUser(id)
    const orders = await fetchOrders(user.id)
    const detail = await fetchOrderDetail(orders[0].id)
    const products = await fetchProducts(detail.productId)
    console.log(products)
  } catch (err) {
    console.error('出错了:', err)
  }
}

并行请求优化

// ❌ 串行:总耗时 = 请求A + 请求B
const a = await fetchA()  // 2秒
const b = await fetchB()  // 3秒
// 总耗时:5秒

// ✅ 并行:总耗时 = max(请求A, 请求B)
const [a, b] = await Promise.all([fetchA(), fetchB()])
// 总耗时:3秒

5. 事件循环(Event Loop)

理解事件循环是掌握异步编程的关键:

console.log('1. 同步代码')

setTimeout(() => {
  console.log('4. 宏任务')
}, 0)

Promise.resolve().then(() => {
  console.log('3. 微任务')
})

console.log('2. 同步代码')

// 输出顺序:1 → 2 → 3 → 4

执行优先级

  1. 同步代码(调用栈)
  2. 微任务(Promise.then、queueMicrotask、MutationObserver)
  3. 宏任务(setTimeout、setInterval、I/O、UI 渲染)

6. 实际应用

请求重试

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const res = await fetch(url)
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      return await res.json()
    } catch (err) {
      if (i === retries - 1) throw err
      await new Promise(r => setTimeout(r, 1000 * (i + 1)))
    }
  }
}

请求取消

const controller = new AbortController()

async function fetchData() {
  try {
    const res = await fetch('/api/data', {
      signal: controller.signal
    })
    return await res.json()
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('请求被取消')
    }
  }
}

// 取消请求
controller.abort()

并发控制

async function asyncPool(limit, items, iteratorFn) {
  const results = []
  const executing = new Set()
  
  for (const item of items) {
    const p = Promise.resolve().then(() => iteratorFn(item))
    results.push(p)
    executing.add(p)
    p.finally(() => executing.delete(p))
    
    if (executing.size >= limit) {
      await Promise.race(executing)
    }
  }
  
  return Promise.all(results)
}

// 限制并发为 3
await asyncPool(3, urls, url => fetch(url))

总结

方案优点缺点
回调简单回调地狱、错误处理困难
Promise链式调用、错误传递仍有嵌套、不够直观
async/await语法清晰、像同步代码需要 Promise 基础

异步编程是 JavaScript 的核心能力之一。理解事件循环、熟练使用 async/await,你就能写出优雅的异步代码!