ES6之Promise的使用

104 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

异步任务的处理

在讲述Promise之前,先让我们来看一个实际案例:

实现一个函数用于发送网络请求(可以使用定时器进行模拟),如果网络请求成功了,就告诉调用者请求发送成功,并将响应数据返回;如果请求失败了,就告诉调用者发送失败,并返回错误信息。

在不使用Promise的情况下,我们的代码实现如下:

function request(url, successCb, failureCb) {
  setTimeout(() => {
    if (url === 'loftyamb.com') {
      successCb('data: 123')
    } else {
      failureCb('err: 404')
    }
  }, 3000)
}

let result
request('loftyam.com', (data) => {
  result = data
  console.log(result);
}, (err) => {
  result = err
  console.log(result);
})

我们通过传入回调函数的方式来实现了它(在请求成功/失败的时候调用对应的回调函数)。

这种方式虽然可以满足我们的需求,但是它存在两个主要的问题:

  1. 我们需要自己来设计回调函数(名称,如何使用);
  2. 对于不同的人,不同的框架设计出来的方案是不同的,那么我们在使用的时候就必须去看他们的文档或者源码才能知道该如何使用这个函数(比如上面案例中的request函数,可以有不同的实现方式);

在这种情况下,出现了一种统一的规范来规范所有代码的编写,它就是Promise(在社区中有Promises/A+规范,后来ECMA中也新增了Promise的规范,并做了一些API的补充)。

Promise的基本使用

Promise是一个类,可译作承诺,许诺,期约;当我们需要给调用者一个承诺:等会我会给你回调数据时,就可以创建一个Promise对象;

在通过new创建Promise对象时,我们需要传入一个回调函数(executor):

  • 这个回调函数会被立刻执行,并且它会被传入两个回调函数resolve和reject;
  • 当在executor中调用resolve时,会执行Promise对象的then方法所传入的回调函数;
  • 当在executor中调用reject时,会执行Promise对象的catch方法所传入的回调函数;

一个Promise对象存在3种状态:

  1. pending(待定):初始状态, 既没有被兑现,也没有被拒绝,当执行executor中的代码时,处于该状态;

fulfilled(已兑现):意味着操作成功完成 , 当执行了resolve时,处于该状态;

  1. rejected(已拒绝): 意味着操作失败, 当执行了reject时,处于该状态;
const promise = new Promise((resolve, reject) => {
  console.log('promise中的代码被立即执行了');
  // resolve('success')
  reject('error')
})

promise.then((value) => {
  console.log('value', value); 
})

promise.catch((reason) => {
  console.log('reason', reason);
})
// promise.catch这种写法相当于是下面这种写法的语法糖
promise.then(undefined, (reason) => {
  console.log('reason', reason);
})

使用Promise重构请求

有了Promise之后,我们可以对开头提出的案例代码实现进行重构了:

function request(url) {
  return new Promise((resolve, reject) => {
    setTimeout(()=> {
      if (url === 'loftyamb.com') {
        resolve(123)
      } else {
        reject(404)
      }

    }, 3000)
  })
}

const promise = request('loftyamb.co')
promise.then((value) => {
  console.log('data:', value);
}, (reason) => {
  console.log('err:', reason);
})

Executor

Executor是在创建Promise对象时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数:

new Promise((resolve, reject) => {
  reject('failure')
  resolve('success')
  console.log('excutor中的代码执行完毕');
}).then((value) => {
  console.log('fulfilled', value);
}, (reason) => {
  console.log('rejected', reason);
})

通常我们会在Executor中确定我们的Promise状态:

  • 通过resolve,可以兑现(fulfilled)Promise的状态,我们也可以称之为已决议(resolved);
  • 通过reject,可以拒绝(reject)Promise的状态;

注意:一旦状态被确定下来,该Promise的状态就会被锁死,不可以再更改

  • 在我们调用resolve的时候,如果resolve传入的值本身不是一个Promise,那么会将该Promise的状态变成兑现(fulfilled);
  • 在之后我们去调用reject是,就不会有任何响应了(并不是这行代码不会执行,而是无法改变Promise的状态);

关于 executor,需理解以下几点:

  • 该 executor 的返回值将被忽略。
  • 如果在该 executor 中抛出一个错误,该 promise 将被拒绝

resolve不同值的区别

  1. 情况一:如果resolve传入的是一个普通值或者对象,那么这个值会作为then回调的参数;
  2. 情况二:如果resolve传入的是另外一个Promise对象,那么这个新Promise对象会决定原Promise的状态;
  3. 情况三:如果resolve传入的是一个对象,并且这个对象实现了then方法,那么会执行该then方法,并根据then方法的结果来决定Promise的状态;

如果一个对象内部实现了then方法,我们就称之为thenable。

const promise = new Promise((resolve,reject) => {
  resolve('123')
})
const promise = new Promise((resolve,reject) => {
  resolve(new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('success')
    }, 3000)
  }))
})
const promise = new Promise((resolve,reject) => {
  resolve({
    name: 'obj',
    then(resolve, reject) {
      resolve('obj')
    }
  })
})

then方法

接受两个参数

then方法是Promise对象上的一个方法:它其实是放在Promise的原型上的 Promise.prototype.then;

then方法接受两个参数:

  1. fulfilled的回调函数(onFulfilled):当状态变成fulfilled时会回调的函数;
  2. reject的回调函数(onRejected):当状态变成reject时会回调的函数;
const promise = new Promise((resolve, reject) => {
  resolve('hhh')
})
promise.then(value => {
  console.log(value)
}, reason => {
  console.log(reason)
})
// 等价于
promise.then(value => {
  console.log(value)
}).catch(reason => {
  console.log(reason)
})

备注: 如果忽略针对某个状态的回调函数参数,或者提供非函数 (nonfunction) 参数,那么 then 方法将会丢失关于该状态的回调函数信息,但是并不会产生错误。如果调用 then 的 Promise 的状态(fulfillment 或 rejection)发生改变,但是 then 中并没有关于这种状态的回调函数,那么 then 将创建一个没有经过回调函数处理的新 Promise 对象,这个新 Promise 只是简单地接受调用这个 then 的原 Promise 的终态作为它的终态。

多次调用

一个Promise的then方法是可以被多次调用的:

  • 每次调用我们都可以传入对应的fulfilled回调;
  • 当Promise的状态变成fulfilled的时候,这些回调函数都会被执行;
const promise = new Promise((resolve, reject) => {
  resolve('hhh')
})

promise.then(value => {
  console.log('onFulfilled1', value);
})
promise.then(value => {
  console.log('onFulfilled2', value);
})
promise.then(value => {
  console.log('onFulfilled3', value);
})

返回值

then方法本身是有返回值的,它的返回值是一个Promise,所以我们可以进行链式调用;

但是then方法返回的Promise到底处于什么样的状态呢?

Promise有三种状态,那么这个Promise处于什么状态呢?

  • 当then方法中的回调函数本身在执行的时候,那么它处于pending状态;
  • 当then方法中的回调函数返回一个结果时,那么它处于fulfilled状态,并且会将结果作为resolve的参数:
    1. 情况一:返回一个普通的值;
    2. 情况二:返回一个Promise;
    3. 情况三:返回一个thenable对象;
  • 当then方法抛出一个异常时,那么它处于reject状态;
// 1> 如果我们返回的是一个普通值(数值/字符串/普通对象/undefined), 
// 那么这个普通的值被作为一个新的Promise的resolve值
promise.then(res => {
   return "aaaaaa"
 }).then(res => {
   console.log("res:", res)
   return "bbbbbb"
 })
// 2> 如果我们返回的是一个Promise
 promise.then(res => {
   return new Promise((resolve, reject) => {
     setTimeout(() => {
      resolve(111111)
     }, 3000)
   })
 }).then(res => {
   console.log("res:", res)
 })
// 3> 如果返回的是一个对象, 并且该对象实现了thenable
promise.then(res => {
  return {
    then: function(resolve, reject) {
      resolve(222222)
    }
  }
}).then(res => {
  console.log("res:", res)
})

catch方法

多次调用

catch方法也是Promise对象上的一个方法,它也是放在Promise的原型上的 Promise.prototype.catch

一个Promise的catch方法是可以被多次调用的:

  • 每次调用我们都可以传入对应的reject回调;
  • 当Promise的状态变成reject的时候,这些回调函数都会被执行;
const promise2 = new Promise((resolve, reject) => {
  reject('11111')
})

promise2.then(res => {}).then(res => {
  throw new Error('then error message')
}).catch(err => {
  console.log('err:', err);
})
// 运行结果
// err:11111

返回值

catch方法和then方法一样,同样会返回一个Promise对象,因此我们可以在catch方法后面继续调用then方法或者catch方法:

const promise = new Promise((resolve, reject) => {
  reject("111111")
})

promise.then(res => {
  console.log("res:", res)
}).catch(err => {
  console.log("err:", err)
  return "catch return value"
}).then(res => {
  console.log("res result:", res)
}).catch(err => {
  console.log("err result:", err)
})
// 运行结果
// err: 111111
// res result: catch return value

从上面这段代码中我们可以看出,正常情况下catch方法返回的Promise对象状态为fulfilled;如果我们希望后续能继续执行catch,那么需要抛出一个异常;

finally方法

  • finally是在ES9(ES2018)中新增的一个特性:表示无论Promise对象无论变成fulfilled还是reject状态,最终都会 被执行的代码;
  • finally方法是不接收参数的,因为无论前面是fulfilled状态,还是reject状态,它都会执行;
new Promise((resolve, reject) => {
  // resolve('1111')
  reject('222')
}).then(value => {
  console.log('fulfilled:', value);
}).catch(reason  => {
  console.log('rejected:', reason);
}).finally(() => {
  console.log('最终总是会被执行');
})

Promise.resolve方法

上面所讲的then,catch,finally方法都属于Promise的实例方法,也就是存在在Promise.prototype上的方法;而Promise也有自己本身的类方法;

如果我们有一个现成的内容了,希望把它转换成Promise对象来实现,这时候我们可以使用Promise.resolve方法来实现;

Promise.resolve的用法相当于new Promise,并执行resolve操作:

// resolve参数的三种情况:
// 1. 普通的值或者对象
Promise.resolve(111).then(value => {
  console.log('fulfilled:', value);
})
Promise.resolve({ name: 'lzh' }).then(value => {
  console.log('fulfilled:', value);
})

// 2.promise对象
Promise.resolve(new Promise((resolve, reject) => {
  // resolve('hhhh')
  reject('xixixi')
})).then(value => {
  console.log('fulfilled:', value);
}, reason => {
  console.log('rejected:', reason);
})

// 3.thenable对象
Promise.resolve({
  name: 'obj',
  then(resolve, reject) {
    resolve('obj')
  }
}).then(value => {
  console.log('fulfilled:', value);
})

Promise.reject方法

它类似于Promise.resolve,只是它会将Promise对象的状态设置为rejected;

Promise.reject的用法相当于new Promise,只是会调用reject;

Promise.reject传入的参数无论是什么形态,都会直接作为reject状态的参数传递到catch的;

Promise.reject('1111').catch(reason  => {
  console.log(reason);
})

Promise.reject(new Promise(reject => reject('hhhh'))).catch(reason => {
  console.log(reason);
})

Promise.reject({
  name: 'obj',
  then(resolve, reject) {
    resolve('obj')
  }
}).catch(reason =>  {
  console.log(reason);
})
/**
 * Promise.reject无论传入的参数是什么,
 * 最终返回的promise对象状态都会变成rejected
 */

Promise.all方法

它的作用是将多个Promise包裹在一起形成一个新的Promise,这个新的Promise状态由包裹的所有Promise共同决定:

  • 当所有的Promise状态变成fulfilled时,新的Promise状态为fulfilled,并且会将所有Promise的返回值组成一个数组;
  • 当有一个Promise状态为rejected时,新的Promise状态为rejected,并且会将第一个rejected的返回值作为参数;
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolve('promise1 fulfilled')
    reject('promise1 rejected')
    console.log('promise1 rejected');
  }, 1000)
  console.log('执行了promise1中的executor执行了');
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise2 fulfilled')
    console.log('promise2 fulfilled');
  }, 2000)
  console.log('执行了promise2中的executor执行了');
})

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise3 fulfilled')
    console.log('promise3 fulfilled');
  }, 3000) 
  
  console.log('执行了promise3中的executor执行了');
})

Promise.all([promise1, promise2, promise3, 'loftyamb']).then(value => {
  console.log(value);
}).catch(reason => {
  console.log(reason);
})
// 执行了promise1中的executor执行了
// 执行了promise2中的executor执行了
// 执行了promise3中的executor执行了
// promise1 rejected
// promise1 rejected
// promise2 fulfilled
// promise3 fulfilled

Promise.allSettled方法

all方法有一个缺陷:当有其中一个Promise变成rejected状态时,新Promise就会立即变成对应的rejected状态,那么对于resolved的,以及依然出于pending状态的额Promise,我们是获取不到对应结果的;

在ES11(ES2020)中,添加了新的API——Promise.allSettled:

  • 该方法会在所有的Promise都有结果(settled)的时候,无论是fulfilled还是rejected时,才会有最终的状态;
  • 并且返回的Promise状态一定是fulfilled的;
const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolve('promise1 fulfilled')
    reject('promise1 rejected')
    console.log('promise1 rejected');
  }, 1000)
  console.log('执行了promise1中的executor执行了');
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise2 fulfilled')
    console.log('promise2 fulfilled');
  }, 2000)
  console.log('执行了promise2中的executor执行了');
})

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise3 fulfilled')
    console.log('promise3 fulfilled');
  }, 3000) 
  
  console.log('执行了promise3中的executor执行了');
})

Promise.allSettled([promise1, promise2, promise3, 'loftyamb']).then(value => {
  console.log(value);
})
// 执行了promise1中的executor执行了
// 执行了promise2中的executor执行了
// 执行了promise3中的executor执行了
// promise1 rejected
// promise2 fulfilled
// promise3 fulfilled
// [
//   { status: 'rejected', reason: 'promise1 rejected' },
//   { status: 'fulfilled', value: 'promise2 fulfilled' },
//   { status: 'fulfilled', value: 'promise3 fulfilled' },
//   { status: 'fulfilled', value: 'loftyamb' }
// ]

可以看到allSettled的打印结果是一个数组,数组中存放的是每一个Promise的结果,这个结果是一个对象的形式,包含status状态,以及对应的value/reason值;

Promsie.race方法

该方法用于当传入的多个Promise对象状态确定时,就作为最终的结果返回;

race表示竞赛,多个Promise相互竞争,谁先有结果,就使用谁的结果;

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise1 fulfilled')
    // reject('promise1 rejected')
    console.log('promise1 fulfilled');
  }, 1000)
  console.log('执行了promise1中的executor执行了');
})

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    // resolve('promise2 fulfilled')
    reject('promise2 rejected')
    console.log('promise2 rejected');
  }, 2000)
  console.log('执行了promise2中的executor执行了');
})

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('promise3 fulfilled')
    console.log('promise3 fulfilled');
  }, 3000) 
  
  console.log('执行了promise3中的executor执行了');
})

Promise.race([promise1, promise2, promise3]).then(value => {
  console.log(value);
}).catch(reason => {
  console.log(reason);
})
// 执行了promise1中的executor执行了
// 执行了promise2中的executor执行了
// 执行了promise3中的executor执行了
// promise1 fulfilled
// promise1 fulfilled
// promise2 rejected
// promise3 fulfilled

Promise.any方法

Promise.any方法是ES12中新增的方法,和race是类似的:

  • any方法会等到一个fulfilled状态,才会决定新的Promise状态;
  • 如果所有的Promsie都是rejected的,那么也会等到所有的Promise都变为rejected状态;
  • 如果所有的Promise都是rejected,那么会报一个AggregateError的错误;
Promise.any([promise1, promise2, promise3]).then(value => {
  console.log(value); // 会打印第一个状态为fulfilled的Promise对象传过来的值
}).catch(reason => {
  console.log(reason); 
  // 如果所有的Promise对象都为rejected,则新Promise的状态为rejected,
  // 并且会抛出AggregateError的错误
})