你对 Promise 的了解?

749 阅读5分钟

今天知乎上有网友提问—— 你对 Promise 的了解?

一、Promise 之前的时代——回调时代

假设我们用 getUser 来说去用户数据,它接收两个回调 sucessCallback 和 errorCallback:

function getUser(successCallback, errorCallback){
  $.ajax({
    url:'/user',
    success: function(response){
      successCallback(response)
    },
    error: function(xhr){
      errorCallback(xhr)  
    }
  })
}

看起来还不算复杂。

如果我们获取用户数据之后还要获取分组数组、分组详情等,代码就会是这样:

getUser(function(response){
    getGroup(response.id, function(group){
      getDetails(groupd.id, function(details){
        console.log(details)
      },function(){
        alert('获取分组详情失败')
      })
    }, function(){
      alert('获取分组失败')
    })
  }, function(){
  alert('获取用户信息失败')
})

三层回调,如果再多一点嵌套,就是「回调地狱」了。

二、Promise 来了

Promise 的思路呢,就是 getUser 返回一个对象,你往这个对象上挂回调:

var promise = getUser()
promise.then(successCallback, errorCallback)

当用户信息加载完毕,successCallback 和 errorCallback 之一就会被执行。

把上面两句话合并成一句就是这样的:

getUser().then(successCallback, errorCallback)

如果你想在用户信息获取结束后做更多事,可以继续 .then:

getUser().then(success1).then(success2).then(success3)

请求成功后,会依次执行 success1、success2 和 success3。

如果要获取分组信息:

getUser().then(function(response){
  getGroup(response.id).then(function(group){
    getDetails(group.id).then(function(){
      
    },error3)
  },error2)
}, error1)

这种 Promise 写法跟前面的回调看起来其实变化不大。

真的,Promise 并不能消灭回调地狱,但是它可以使回调变得可控。你对比下面两个写法就知道了。

getGroup(response.id, success2, error2)

getGroup(response.id).then(success2, error2)

用 Promise 之前,你不能确定 success2 是第几个参数;

用 Promise 之后,所有的回调都是

.then(success, error) 

这样的形式。

三、Promise 的用途

总结一下上面说的内容:Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。

四、如何创建一个 new Promise

  • return new Promise((resolve,reject)=>{...})
  • 任务成功调用resolve(result)
  • 任务失败调用reject(error)
  • resolve和reject 会再去调用成功和失败函数
  • 使用 .then(success,fail)传入成功和失败函数 下面演示使用方法:
var promise = new Promise(function (resolve, reject) {
  // ...

  if (/* 异步操作成功 */){
    resolve(value);
  } else { /* 异步操作失败 */
    reject(new Error());
  }
});

上面代码中,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。

resolve函数的作用是,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。

reject函数的作用是,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

五、如何使用 Promise.prototype.then(来源 MDN)

Promise 实例的then方法,用来添加回调函数。

then方法可以接受两个回调函数,第一个是异步操作成功时的回调函数,第二个是异步操作失败时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。 then方法可以链式使用。

p1
  .then(step1)
  .then(step2)
  .then(step3)
  .then(
    console.log,
    console.error
  );

上面代码中,p1后面有四个then,意味依次有四个回调函数。只要前一步的状态变为fulfilled,就会依次执行紧跟在后面的回调函数。

最后一个then方法,回调函数是console.logconsole.error,用法上有一点重要的区别。console.log只显示step3的返回值,而console.error可以显示p1step1step2step3之中任意一个发生的错误。举例来说,如果step1的状态变为rejected,那么step2step3都不会执行了(因为它们是resolved的回调函数)。Promise 开始寻找,接下来第一个为rejected的回调函数,在上面代码中是console.error。这就是说,Promise 对象的报错具有传递性。

六、如何使用 Promise.all(来源 MDN)

方法返回一个Promise实例,此实例在 iterable 参数内所有的promise 都完成(resolved)时回调完成(resolve);如果参数中 promise有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败promise的结果。

Promise2.all = function(arrP) {
  let list = []
  let len = 0
  let hasErr = false
  return new Promise2((resolve, reject) => {
    for(let i = 0; i < arrP.length; i++) {
      arrP[i].then( data=> {
        list[i] = data
        len++
        len === arrP.length && resolve(list)
      }, error => {
        !hasErr && reject(error)
        hasErr = true
      })
    }
  })
}

七、如何使用 Promise.race(来源 MDN)

方法返回一个Promise实例,一旦迭代器中的某个 promise 完成(resolved)或失败(rejected),返回的 promise 就会 resolve 或 reject

Promise2.race = function(arrP) {
  let hasValue = false
  let hasError = false
  return new Promise2((resolve, reject) => {
    for(let i = 0; i < arrP.length; i++) {
      arrP[i].then(data => {
        !hasValue && !hasError && resolve(data) 
        hasValue = true
      }, error => {
        !hasValue && !hasError &&reject(error)
        hasError = true
      })
    }
  })
}

八、小结

Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。

而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。

Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。