【AJAX-Day2】Promise与回调地狱

2 阅读5分钟

【AJAX-Day2】Promise与回调地狱

🎯 核心目标:彻底理解 Promise 三种状态、链式调用、async/await、以及如何解决回调地狱


一、回调地狱(Callback Hell)

1.1 问题的产生

当多个异步操作有依赖关系时(必须等上一个完成才能执行下一个),就会产生层层嵌套的回调:

// 场景:登录 → 获取用户信息 → 获取用户的订单 → 获取订单详情
// 每一步都依赖上一步的结果

// 原生 XHR 回调地狱版本
login({ username, password }, function(userData) {
  getUserInfo(userData.id, function(userInfo) {
    getOrders(userInfo.id, function(orders) {
      getOrderDetail(orders[0].id, function(detail) {
        // 终于拿到了……但代码已经缩进到了宇宙深处
        console.log(detail)
        // 更深的嵌套...
      }, function(err) { console.error(err) })
    }, function(err) { console.error(err) })
  }, function(err) { console.error(err) })
}, function(err) { console.error(err) })

回调 地狱的问题:

  • 代码横向扩展,难以阅读
  • 错误处理分散,难以维护
  • 代码逻辑难以复用
  • 无法使用 try/catch

1.2 Promise 的诞生

Promise 是 ES6 引入的异步编程 解决方案,它将异步操作封装成一个对象,通过链式调用解决回调地狱。


二、Promise 详解

2.1 Promise 的三种状态

pending(等待中)
    ↓
  成功 → fulfilled(已完成)→ 执行 .then() 的回调
  失败 → rejected(已拒绝)→ 执行 .catch() 的回调

⚠️ 状态一旦改变,就不会再变(不可逆)
// 创建 Promise
const p = new Promise((resolve, reject) => {
  // 执行异步操作
  setTimeout(() => {
    const success = Math.random() > 0.5

    if (success) {
      resolve('成功的数据')  // 状态:pending → fulfilled
    } else {
      reject(new Error('失败的原因'))  // 状态:pending → rejected
    }
  }, 1000)
})

// 消费 Promise
p.then(data => {
  console.log('成功:', data)  // 'success的数据'
}).catch(err => {
  console.error('失败:', err.message)
})

2.2 Promise 链式调用

then() 返回一个新的 Promise,这是链式调用的关键。

// 链式调用解决回调地狱
axios.get('/api/login', { params: { username, password } })
  .then(({ data }) => {
    // 第一步成功,返回值会传给下一个 then
    return axios.get(`/api/users/${data.userId}`)
  })
  .then(({ data }) => {
    // 第二步成功
    return axios.get(`/api/orders?userId=${data.id}`)
  })
  .then(({ data }) => {
    // 第三步成功
    console.log('订单列表:', data)
  })
  .catch(err => {
    // 任意一步失败都会被这里捕获
    console.error('出错了:', err.message)
  })

then() 的返回值规则:

promise
  .then(data => {
    return 123           // 返回普通值 → 下一个 then 收到 123
  })
  .then(data => {
    return axios.get('/api/xxx')  // 返回 Promise → 等待该 Promise 完成
  })
  .then(data => {
    // data 是上面 axios.get 的结果
    throw new Error('主动抛出错误')  // 抛出错误 → 跳转到 catch
  })
  .catch(err => { ... })

2.3 Promise 静态方法

// Promise.resolve():创建一个立即成功的 Promise
const p1 = Promise.resolve(42)
p1.then(v => console.log(v))  // 42

// Promise.reject():创建一个立即失败的 Promise
const p2 = Promise.reject(new Error('失败'))
p2.catch(e => console.error(e))

// Promise.all():所有都成功才成功,有一个失败就失败
Promise.all([
  axios.get('/api/users'),
  axios.get('/api/posts'),
  axios.get('/api/comments')
]).then(([users, posts, comments]) => {
  // 三个请求都完成后才执行
  console.log(users.data, posts.data, comments.data)
}).catch(err => {
  // 任意一个失败则触发
  console.error(err)
})

// Promise.allSettled():所有都完成(无论成功失败)才结束(ES2020)
Promise.allSettled([
  axios.get('/api/users'),
  axios.get('/api/will-fail')
]).then(results => {
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('成功:', result.value)
    } else {
      console.log('失败:', result.reason)
    }
  })
})

// Promise.race():第一个完成的(无论成功失败)决定结果
// 应用:超时控制
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('请求超时')), ms)
  )
  return Promise.race([promise, timeout])
}

// Promise.any():第一个成功的决定结果(ES2021)
Promise.any([
  fetch('https://api1.example.com'),  // 可能失败
  fetch('https://api2.example.com'),  // 备用地址
]).then(response => {
  // 用最先成功的那个
})

三、async / await

3.1 什么是 async/await?

async/await 是 ES2017 引入的语法糖,让异步代码写起来像同步代码一样直观。

// Promise 链式调用版
function getData() {
  return axios.get('/api/step1')
    .then(r1 => axios.get(`/api/step2/${r1.data.id}`))
    .then(r2 => axios.get(`/api/step3/${r2.data.id}`))
    .then(r3 => r3.data)
    .catch(err => console.error(err))
}

// async/await 版(更清晰!)
async function getData() {
  try {
    const r1 = await axios.get('/api/step1')
    const r2 = await axios.get(`/api/step2/${r1.data.id}`)
    const r3 = await axios.get(`/api/step3/${r2.data.id}`)
    return r3.data
  } catch (err) {
    console.error(err)
  }
}

3.2 async 函数的特点

// async 函数总是返回一个 Promise
async function fn() {
  return 42  // 等价于 return Promise.resolve(42)
}
fn().then(v => console.log(v))  // 42

// await 只能在 async 函数内使用
async function example() {
  // await 等待 Promise resolve,拿到值
  const result = await Promise.resolve('hello')
  console.log(result)  // 'hello'
  
  // await 暂停当前 async 函数,但不阻塞主线程!
  const data = await axios.get('/api/data')
  // data 就是 axios 响应对象(不是 Promise)
  console.log(data.data)
}

3.3 错误处理

// 方式一:try/catch(推荐,清晰)
async function fetchUser(id) {
  try {
    const { data } = await axios.get(`/api/users/${id}`)
    return data
  } catch (error) {
    console.error('获取用户失败:', error.response?.data?.message)
    return null
  }
}

// 方式二:.catch() 链(适合单个请求)
async function fetchUser(id) {
  const { data } = await axios.get(`/api/users/${id}`)
    .catch(err => {
      console.error(err)
      return { data: null }
    })
  return data
}

// 方式三:封装 await 的错误处理(进阶)
async function to(promise) {
  try {
    const result = await promise
    return [null, result]
  } catch (error) {
    return [error, null]
  }
}

async function fetchUser(id) {
  const [err, { data }] = await to(axios.get(`/api/users/${id}`))
  if (err) {
    console.error(err)
    return null
  }
  return data
}

3.4 并发请求(避免串行等待)

// ❌ 串行(效率低,共等待 3 秒)
async function slowVersion() {
  const r1 = await axios.get('/api/a')  // 等 1 秒
  const r2 = await axios.get('/api/b')  // 等 1 秒
  const r3 = await axios.get('/api/c')  // 等 1 秒
  return [r1.data, r2.data, r3.data]
}

// ✅ 并发(效率高,只等最长的那个)
async function fastVersion() {
  const [r1, r2, r3] = await Promise.all([
    axios.get('/api/a'),
    axios.get('/api/b'),
    axios.get('/api/c')
  ])
  return [r1.data, r2.data, r3.data]
}

四、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(value))
      }
    }

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

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

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        try {
          const result = onFulfilled(this.value)
          resolve(result)
        } catch (e) { reject(e) }
      }
      if (this.state === 'rejected') {
        try {
          const result = onRejected(this.reason)
          resolve(result)
        } catch (e) { reject(e) }
      }
      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(v => {
          try { resolve(onFulfilled(v)) } catch (e) { reject(e) }
        })
        this.onRejectedCallbacks.push(r => {
          try { resolve(onRejected(r)) } catch (e) { reject(e) }
        })
      }
    })
  }
}

五、知识图谱

Promise 与回调地狱
├── 回调地狱
│   ├── 原因:异步依赖导致嵌套
│   └── 问题:可读性差、错误分散、难维护
├── Promise
│   ├── 三种状态:pending → fulfilled/rejected(不可逆)
│   ├── 创建:new Promise((resolve, reject) => {...})
│   ├── 消费:.then(成功) / .catch(失败) / .finally(都执行)
│   ├── 链式:then 返回新 Promise,可串联
│   └── 静态方法
│       ├── all:全部成功才成功
│       ├── allSettled:全部完成(不管成败)
│       ├── race:第一个完成的决定结果
│       └── any:第一个成功的决定结果
└── async/await
    ├── async 函数返回 Promise
    ├── await 等待 Promise,暂停当前函数(不阻塞线程)
    ├── 错误处理:try/catch
    └── 并发:Promise.all 并行,避免串行等待

六、高频面试题

Q1:Promise 的三种状态是什么?状态能否改变?

pending(等待)、fulfilled(成功)、rejected(失败)。状态一旦从 pending 变为 fulfilled 或 rejected,就不可再改变(不可逆)。

Q2:async/await 和 Promise 的关系?

async/await 是 Promise 的语法糖。async 函数返回一个 Promise;await 只是 .then() 的更优雅写法,它暂停当前 async 函数执行,等待 Promise resolve 后继续,但不阻塞 JS 线程(事件循环继续运行)。

Q3:Promise.all 和 Promise.allSettled 的区别?

Promise.all:只要有一个 rejected 就立即失败,适合所有请求都必须成功的场景;Promise.allSettled:等所有 Promise 都完成(无论成败),返回每个的状态和值/原因,适合需要知道所有结果的场景。

Q4:以下代码的输出顺序?

console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// 输出:1 4 3 2
// 同步先执行(1,4),微任务优先(3),最后宏任务(2)

⬅️ 上一篇Day1 - HTTP协议与XHR基础 ➡️ 下一篇Day3 - Axios深入与请求拦截