JS 异步编程实战 | 从回调地狱到 Promise/Async/Await(附代码 + 面试题)

23 阅读7分钟

一、为什么需要异步编程?

JavaScript 是单线程语言,同一时间只能做一件事。如果有耗时操作(如网络请求、文件读取、定时任务),就会阻塞后续代码执行。

// 同步阻塞示例 
console.log('开始')
for(let i = 0; i < 1000000000; i++) {}
// 耗时操作 console.log('结束') 
// 必须等待循环结束才执行

为了解决这个问题,JavaScript 提供了异步编程解决方案。

二、回调函数(Callback)—— 最基础的异步方案

2.1 基本概念

回调函数是将函数作为参数传递给另一个函数,在异步操作完成后调用。

// 模拟异步请求
function fetchData(callback) {
  setTimeout(() => {
    callback('数据加载完成')
  }, 1000)
}

console.log('开始请求')
fetchData((data) => {
  console.log(data) // 1秒后输出:数据加载完成
})
console.log('继续执行其他操作')
// 输出顺序:开始请求 → 继续执行其他操作 → 数据加载完成

2.2 回调地狱的产生

当有多个依赖的异步操作时,回调嵌套会形成"回调地狱":

// 回调地狱示例
getUserInfo(function(user) {
  getOrderList(user.id, function(orders) {
    getOrderDetail(orders[0].id, function(detail) {
      getProductInfo(detail.productId, function(product) {
        console.log('最终数据:', product)
      }, function(error) {
        console.error('获取商品失败', error)
      })
    }, function(error) {
      console.error('获取订单详情失败', error)
    })
  }, function(error) {
    console.error('获取订单列表失败', error)
  })
}, function(error) {
  console.error('获取用户失败', error)
})

回调地狱的问题:

  • 代码难以阅读和维护
  • 错误处理分散
  • 难以复用和调试

三、Promise —— 优雅的异步解决方案

3.1 Promise 基本用法

Promise 是 ES6 引入的异步编程解决方案,它代表一个异步操作的最终完成或失败。

// 创建 Promise
const promise = new Promise((resolve, reject) => {
  // 执行异步操作
  setTimeout(() => {
    const success = true
    if (success) {
      resolve('操作成功') // 成功时调用
    } else {
      reject('操作失败') // 失败时调用
    }
  }, 1000)
})

// 使用 Promise
promise
  .then(result => {
    console.log(result) // 成功:操作成功
  })
  .catch(error => {
    console.error(error) // 失败:操作失败
  })
  .finally(() => {
    console.log('无论成功失败都会执行')
  })

3.2 解决回调地狱

使用 Promise 重构上面的例子:

// 将每个异步操作封装成 Promise
function getUserInfo() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id: 1, name: '张三' })
    }, 1000)
  })
}

function getOrderList(userId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([{ id: 101, name: '订单1' }, { id: 102, name: '订单2' }])
    }, 1000)
  })
}

function getOrderDetail(orderId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id: orderId, productId: 1001, price: 299 })
    }, 1000)
  })
}

function getProductInfo(productId) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({ id: productId, name: '商品名称', price: 299 })
    }, 1000)
  })
}

// 链式调用,告别回调地狱
getUserInfo()
  .then(user => {
    console.log('用户:', user)
    return getOrderList(user.id)
  })
  .then(orders => {
    console.log('订单列表:', orders)
    return getOrderDetail(orders[0].id)
  })
  .then(detail => {
    console.log('订单详情:', detail)
    return getProductInfo(detail.productId)
  })
  .then(product => {
    console.log('商品信息:', product)
  })
  .catch(error => {
    console.error('发生错误:', error)
  })

3.3 Promise 静态方法

// Promise.all - 等待所有 Promise 完成
const p1 = Promise.resolve(3)
const p2 = 42
const p3 = new Promise((resolve) => setTimeout(resolve, 100, 'foo'))

Promise.all([p1, p2, p3]).then(values => {
  console.log(values) // [3, 42, "foo"]
})

// Promise.race - 返回最先完成的 Promise
const promise1 = new Promise(resolve => setTimeout(resolve, 500, 'one'))
const promise2 = new Promise(resolve => setTimeout(resolve, 100, 'two'))

Promise.race([promise1, promise2]).then(value => {
  console.log(value) // "two" (因为 promise2 更快)
})

// Promise.allSettled - 等待所有 Promise 完成(无论成功失败)
const promises = [
  Promise.resolve('成功1'),
  Promise.reject('失败2'),
  Promise.resolve('成功3')
]

Promise.allSettled(promises).then(results => {
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('成功:', result.value)
    } else {
      console.log('失败:', result.reason)
    }
  })
})

// Promise.any - 返回第一个成功的 Promise
const pErr = new Promise((resolve, reject) => reject('总是失败'))
const pSlow = new Promise(resolve => setTimeout(resolve, 500, '最终完成'))
const pFast = new Promise(resolve => setTimeout(resolve, 100, '很快完成'))

Promise.any([pErr, pSlow, pFast]).then(value => {
  console.log(value) // "很快完成"
})

四、Async/Await —— 同步方式的异步编程

4.1 基本语法

Async/Await 是 ES2017 引入的语法糖,让异步代码看起来像同步代码。

// async 函数返回一个 Promise
async function getData() {
  return '数据'
}

getData().then(result => console.log(result)) // 数据

// 使用 await 等待 Promise 完成
async function fetchUserData() {
  try {
    const user = await getUserInfo()
    console.log('用户:', user)
    
    const orders = await getOrderList(user.id)
    console.log('订单:', orders)
    
    const detail = await getOrderDetail(orders[0].id)
    console.log('详情:', detail)
    
    const product = await getProductInfo(detail.productId)
    console.log('商品:', product)
    
    return product
  } catch (error) {
    console.error('出错了:', error)
  }
}

// 调用 async 函数
fetchUserData().then(result => {
  console.log('最终结果:', result)
})

4.2 实战示例:模拟数据请求

// 模拟 API 请求函数
const mockAPI = (url, delay = 1000) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.1) { // 90% 成功率
        resolve({
          status: 200,
          data: { url, timestamp: Date.now() }
        })
      } else {
        reject(new Error(`请求 ${url} 失败`))
      }
    }, delay)
  })
}

// 使用 async/await 实现并发请求
async function fetchMultipleData() {
  try {
    // 并发请求
    const [userData, productData, orderData] = await Promise.all([
      mockAPI('/api/user', 800),
      mockAPI('/api/product', 1200),
      mockAPI('/api/order', 600)
    ])
    
    console.log('所有数据加载完成:')
    console.log('用户数据:', userData.data)
    console.log('商品数据:', productData.data)
    console.log('订单数据:', orderData.data)
    
    return { userData, productData, orderData }
  } catch (error) {
    console.error('数据加载失败:', error.message)
  }
}

// 串行请求(依赖关系)
async function fetchDependentData() {
  console.time('串行请求耗时')
  
  const user = await mockAPI('/api/user', 1000)
  console.log('第一步完成:', user.data)
  
  const orders = await mockAPI(`/api/user/${user.data.url}/orders`, 1000)
  console.log('第二步完成:', orders.data)
  
  const details = await mockAPI(`/api/orders/${orders.data.url}/details`, 1000)
  console.log('第三步完成:', details.data)
  
  console.timeEnd('串行请求耗时')
  // 总耗时约 3000ms
}

// 优化:并行处理不依赖的数据
async function fetchOptimizedData() {
  console.time('优化后耗时')
  
  // 同时发起两个独立请求
  const [user, products] = await Promise.all([
    mockAPI('/api/user', 1000),
    mockAPI('/api/products', 1000)
  ])
  
  console.log('用户和商品数据已获取')
  
  // 依赖用户数据的请求
  const orders = await mockAPI(`/api/user/${user.data.url}/orders`, 1000)
  
  // 可以并行处理的请求
  const [detail1, detail2] = await Promise.all([
    mockAPI(`/api/orders/${orders.data.url}/detail1`, 500),
    mockAPI(`/api/orders/${orders.data.url}/detail2`, 500)
  ])
  
  console.timeEnd('优化后耗时')
  // 总耗时约 2500ms
}

4.3 错误处理最佳实践

// 统一的错误处理函数
const handleAsyncError = (asyncFn) => {
  return async (...args) => {
    try {
      return [await asyncFn(...args), null]
    } catch (error) {
      return [null, error]
    }
  }
}

// 使用错误处理包装器
const safeFetchUser = handleAsyncError(fetchUserData)

async function main() {
  const [user, error] = await safeFetchUser()
  
  if (error) {
    console.error('操作失败:', error.message)
    return
  }
  
  console.log('操作成功:', user)
}

// 带超时的 Promise
function withTimeout(promise, timeout = 5000) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('请求超时')), timeout)
  })
  
  return Promise.race([promise, timeoutPromise])
}

async function fetchWithTimeout() {
  try {
    const result = await withTimeout(mockAPI('/api/data', 3000), 2000)
    console.log('数据:', result)
  } catch (error) {
    console.error('超时或失败:', error.message)
  }
}

五、手写实现(面试高频)

5.1 手写 Promise

class MyPromise {
  constructor(executor) {
    this.state = 'pending'
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled'
        this.value = value
        this.onFulfilledCallbacks.forEach(fn => fn())
      }
    }

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected'
        this.reason = reason
        this.onRejectedCallbacks.forEach(fn => fn())
      }
    }

    try {
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }

    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value)
            this.resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      }

      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason)
            this.resolvePromise(promise2, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0)
      }

      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value)
              this.resolvePromise(promise2, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0)
        })

        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason)
              this.resolvePromise(promise2, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0)
        })
      }
    })

    return promise2
  }

  resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
      reject(new TypeError('Chaining cycle detected'))
    }

    if (x && (typeof x === 'object' || typeof x === 'function')) {
      let called = false
      try {
        const then = x.then
        if (typeof then === 'function') {
          then.call(
            x,
            y => {
              if (called) return
              called = true
              this.resolvePromise(promise2, y, resolve, reject)
            },
            error => {
              if (called) return
              called = true
              reject(error)
            }
          )
        } else {
          resolve(x)
        }
      } catch (error) {
        if (called) return
        called = true
        reject(error)
      }
    } else {
      resolve(x)
    }
  }

  catch(onRejected) {
    return this.then(null, onRejected)
  }

  static resolve(value) {
    if (value instanceof MyPromise) return value
    return new MyPromise(resolve => resolve(value))
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason))
  }

  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const result = []
      let count = 0
      
      for (let i = 0; i < promises.length; i++) {
        MyPromise.resolve(promises[i]).then(
          value => {
            result[i] = value
            count++
            if (count === promises.length) resolve(result)
          },
          reject
        )
      }
    })
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      for (const promise of promises) {
        MyPromise.resolve(promise).then(resolve, reject)
      }
    })
  }
}

5.2 手写 async/await 的简单实现

// 使用 Generator 模拟 async/await
function asyncToGenerator(generatorFn) {
  return function() {
    const gen = generatorFn.apply(this, arguments)
    
    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let result
        try {
          result = gen[key](arg)
        } catch (error) {
          reject(error)
          return
        }
        
        const { value, done } = result
        
        if (done) {
          resolve(value)
        } else {
          Promise.resolve(value).then(
            val => step('next', val),
            err => step('throw', err)
          )
        }
      }
      
      step('next')
    })
  }
}

// 使用示例
const fetchData = function() {
  return new Promise(resolve => {
    setTimeout(() => resolve('数据'), 1000)
  })
}

const getData = asyncToGenerator(function* () {
  const data1 = yield fetchData()
  console.log('data1:', data1)
  
  const data2 = yield fetchData()
  console.log('data2:', data2)
  
  return '完成'
})

getData().then(result => console.log(result))

六、面试高频题

6.1 输出顺序题

// 题目1
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')

// 输出:1, 4, 3, 2
// 解释:同步代码先执行,微任务(Promise)先于宏任务(setTimeout)

// 题目2
async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

console.log('script start')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

async1()

new Promise((resolve) => {
  console.log('promise1')
  resolve()
}).then(() => {
  console.log('promise2')
})

console.log('script end')

// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

6.2 错误处理题

// 题目:如何捕获 async/await 的错误?
async function getData() {
  try {
    const data = await Promise.reject('出错了')
    console.log(data)
  } catch (error) {
    console.log('捕获到:', error)
  }
}

// 或使用 .catch
async function getData2() {
  const data = await Promise.reject('出错了').catch(err => {
    console.log('处理错误:', err)
    return '默认值'
  })
  console.log(data) // 默认值
}

// 题目:Promise.all 的错误处理
const promises = [
  Promise.resolve(1),
  Promise.reject('错误'),
  Promise.resolve(3)
]

Promise.all(promises)
  .then(console.log)
  .catch(console.error) // 输出:错误

// 如何让 Promise.all 即使有错误也返回所有结果?
Promise.allSettled(promises).then(results => {
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('成功:', result.value)
    } else {
      console.log('失败:', result.reason)
    }
  })
})

6.3 并发控制题

// 题目:实现一个并发控制器,限制同时执行的 Promise 数量
class PromiseQueue {
  constructor(concurrency = 2) {
    this.concurrency = concurrency
    this.running = 0
    this.queue = []
  }
  
  add(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject })
      this.run()
    })
  }
  
  run() {
    while (this.running < this.concurrency && this.queue.length) {
      const { task, resolve, reject } = this.queue.shift()
      this.running++
      
      Promise.resolve(task())
        .then(resolve, reject)
        .finally(() => {
          this.running--
          this.run()
        })
    }
  }
}

// 使用示例
const queue = new PromiseQueue(2)

for (let i = 0; i < 5; i++) {
  queue.add(() => 
    new Promise(resolve => {
      setTimeout(() => {
        console.log(`任务${i}完成`)
        resolve(i)
      }, 1000)
    })
  )
}
// 每2个任务并行执行

6.4 重试机制题

// 题目:实现一个函数,请求失败时自动重试
async function retryRequest(fn, maxRetries = 3, delay = 1000) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      console.log(`第${i + 1}次尝试`)
      const result = await fn()
      console.log('请求成功')
      return result
    } catch (error) {
      console.log(`第${i + 1}次失败`)
      if (i === maxRetries - 1) {
        throw error
      }
      // 等待延迟时间后重试
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
}

// 使用示例
let attempt = 0
const request = () => {
  return new Promise((resolve, reject) => {
    attempt++
    if (attempt < 3) {
      reject('模拟失败')
    } else {
      resolve('成功')
    }
  })
}

retryRequest(request, 3, 1000)
  .then(console.log)
  .catch(console.error)

七、总结与建议

7.1 异步编程演进

  • 回调函数:基础但容易形成"回调地狱"
  • Promise:链式调用,错误统一处理
  • Async/Await:语法糖,代码更直观

7.2 使用建议

  1. 优先使用 async/await,代码更清晰
  2. 并发请求使用 Promise.all,提高性能
  3. 注意错误处理,不要吞掉错误
  4. 避免回调地狱,及时重构代码
  5. 理解事件循环,掌握执行顺序

7.3 面试准备

  • 掌握三种异步方案的原理和用法
  • 能够手写简单的 Promise
  • 理解宏任务和微任务的执行顺序
  • 熟悉常见的异步编程场景和解决方案
  • 能够处理并发控制和错误重试

异步编程是 JavaScript 的核心特性,掌握好这块内容不仅对面试有帮助,更能提升实际开发中的代码质量。