初识 Promise - 避免回调地狱

113 阅读4分钟

简介

Promise 是 ES6 引入的一个新的概念,它是一种对异步操作进行封装的方式。它可以使异步操作更加灵活、更加清晰、更加易于维护。

  • 有三个状态
    • Pending(等待状态)、 Fulfilled(成功状态)、Rejected(失败状态)
    • 在 Promise 对象的生命周期中,可以从 Pending 状态转换到 Fulfilled 或 Rejected 状态。
    • 它支持链式调用,可以解决传统回调函数存在的 “回调地狱” 问题。
  • 有两个关键的方法then() catch()
    • promise.then()方法用于指定在 Promise 对象状态从 pending 变为 fulfilled 时的回调函数,
    • promise.catch()方法用于指定在 Promise 对象状态从 pending 变为 rejected 时的回调函数。

Promise 可以封装任何异步操作,包括但不限于:基于回调函数的异步编程、事件监听与处理、兼容其他异步编程库等。当需要进行异步编程时,可以考虑使用 Promise 封装异步操作,从而避免回调函数嵌套带来的困扰。

回调地狱

回调地狱是指在异步编程中,嵌套多层回调函数导致代码可读性变差、难以维护,进而带来困扰的现象。在回调地狱中,一个异步操作导致了另外一个异步操作,而后者的回调函数又导致了另外一个异步操作,以此类推。这种操作层层嵌套的方式导致代码变得难以理解、难以维护。

例如:

假设我们需要进行异步操作来从服务器上获取用户信息,然后根据用户信息再发起两个异步操作,依次从服务器上获取用户订单和用户地址信息

getUserInfo((userInfo) => {
  getOrderInfo(userInfo.userId, (orderInfo) => {
    getAddressInfo(userInfo.userId, (addressInfo) => {
      console.log("用户信息:" + userInfo);
      console.log("订单信息:" + orderInfo);
      console.log("地址信息:" + addressInfo);
    });
  });
});

在这个例子中,我们需要先获取用户信息,然后根据用户信息获取订单和地址信息。由于异步操作需要等待服务器的响应,因此我们需要使用回调函数来处理异步操作的结果。然而由于需要进行多个异步操作,因此这些回调函数被嵌套在一起,导致了回调地狱的出现,一旦嵌套的层数变多,代码将变得非常臃肿和难以理解。

  • 这里仅仅是举例三个回调,如果我们想要操作多个异步函数,就容易看不懂,难理解。除此之外还会存在两个问题:
    • 命名不规范。
    • 不能很好地捕获错误,很难进行错误处理

Promise 解决回调问题

  • 如何解决回调地狱问题 ?
    • 规范回调的名字或顺序
    • 拒绝回调地狱,让代码可读性更强
    • 很方便地捕获错误

我们利用 Promise 可以完美解决以上三个问题。例如对上面的例子我们可以用 Promise 改写:

getUserInfo()
.then((userInfo) => {
  return getOrderInfo(userInfo.userId);
})
.then((orderInfo) => {
  return getAddressInfo(userInfo.userId);
})
.then((addressInfo) => {
  console.log("用户信息:" + userInfo);
  console.log("订单信息:" + orderInfo);
  console.log("地址信息:" + addressInfo);
})
.catch((err) => {
  console.log(err);
});

在这个例子中,我们首先调用 getUserInfo 方法来获得用户信息。在 then 方法中,我们返回一个 Promise,该 Promise 执行 getOrderInfo 方法。当 getOrderInfo 方法完成后,它将返回一个 Promise,该 Promise 在下一个 then 方法中执行 getAddressInfo 方法。最后,当 getAddressInfo 方法完成时,最后一个 then 会执行。如果任何一个异步操作失败,就会跳转到 catch 方法中进行错误处理。

以 AJAX 的封装为例,来解释一下 Promise 的用法

  • 封装一个AJAX
ajax = (method, url, options)=>{
  const {success, fail} = options   //析构赋值,es6新语法。表示从options中获取success和fail。等于:const success = options.success ; const fail = options.fail
  const request = new XMLHttpRequest()
  request.open(method, url)
  request.onreadystatechange = ()=>{
    if(request.readyState === 4){  //成功就调用 success,失败就调用 fail
      if(request.status < 400){
        success.call(null, request.response)  //this为null
      }else if(request.status >= 400){
        fail.call(null, request, request.status)
      }
    }
  }
  request.send()
}
  • 封装这样一个ajax后,之后可以直接使用下面的方法:
ajax('get', '/xxx', { 
  success(response){}, fail: (request, status)=>{} 
}) // 左边是 function 缩写,右边是箭头函数
  • 改用 Promise 的方法:
ajax('get', '/xxx')
  .then((response)=>{}, (request)=>{})

虽然也是回调。但是不需要记 success 和 fail 了

  • then 的第一个参数就是 success
  • then 的第二个参数就是 fail
  • promise规范 success 在前,fail 在后

在这里 ajax() 返回了一个含有 .then() 方法的对象。如何得到这个含有 .then() 的对象呢?那就要改造 ajax 的源码了,如下:

ajax = (method, url, options)=>{
  return new Promise((resolve, reject)=>{
    const {success, fail} = options
    const request = new XMLHttpRequest()
    request.open(method, url)
    request.onreadystatechange = ()=>{
      if(request.readyState === 4){  // 成功就调用 resolve,失败就调用 reject
        if(request.status < 400){
          resolve.call(null, request.response)
        }else if(request.status >= 400){
          reject.call(null, request)
        }
      }
    }
    request.send()
  })
}

直接返回一个构造函数 PromisePromise 接受一个箭头函数,包裹了ajax 函数体。箭头函数必须声明两个参数(resolve, reject),注意和 then 的两个参数不是同一个。它们的.call()只接受一个参数,并且this是空。

总结

如何将一个回调的异步函数变成promise异步函数?

  • 第一步
    • return new Promise((resolve,reject)=>{...})
    • 任务成功则调用 resolve(result)
    • 任务失败则调用 reject(error)
    • resolve 和 reject 会再去调用成功和失败函数
  • 第二步
    • 使用 .then(success, fail) 传入成功和失败函数

资料来源:饥人谷