toFrankie / blog

种一棵树,最好的时间是十年前。其次,是现在。
21 stars 1 forks source link

细读 ES6 | Promise 下篇 #256

Open toFrankie opened 1 year ago

toFrankie commented 1 year ago

配图源自 Freepik

上一篇,继续介绍了 Promise 相关 API。

一、Promise.resolve()

Promise.resolve() 方法的作用就是将某个值(非 Promise 对象实例)转换为 Promise 对象实例。

const promise = Promise.resolve('foo')
// 相当于
const promise = new Promise(resolve => resolve('foo'))

需要注意的是,它仍然会遵循 Event Loop 机制,包括后面介绍的其他 API。具体执行顺序本文不展开讨论。

Promise.resolve() 方法的参数分为四种情况:

1. 不带任何参数

它返回一个状态为 fulfilled,值为 undefinedPromise 实例对象。

const promise = Promise.resolve()

// promise 结果:
// {
//   [[Prototype]]: Promise,
//   [[PromiseState]]: "fulfilled",
//   [[PromiseResult]]: undefined
// }
2. 参数是一个 Promise 实例对象

这时,Promise.resolve() 将会不做任何修改、原封不动地返回该实例。

请注意,即使参数是一个 rejected 状态的 Promise 实例,返回的实例也不会变成 fulfilled 状态,不要被这个 resolve 字面意思误解了。

const p1 = new Promise(resolve => resolve({ name: 'Frankie' })) // "fulfilled"
const p2 = new Promise((resolve, reject) => reject({ name: 'Frankie' })) // "rejected"
const p3 = Promise.resolve(p1)
const p4 = Promise.resolve(p2)

console.log(p1 === p3) // true
console.log(p2 === p4) // true

// p3 结果:
// {
//   [[Prototype]]: Promise,
//   [[PromiseState]]: "fulfilled",
//   [[PromiseResult]]: { name: 'Frankie' }
// }

// p4 结果:
// {
//   [[Prototype]]: Promise,
//   [[PromiseState]]: "rejected",
//   [[PromiseResult]]: { name: 'Frankie' }
// }

其实这种情况,就是上一篇提到过的。

const p5 = new Promise(resolve => resovle(1))
const p6 = new Promise(resolve => {
  reslove(p5)
  // 注意,不要尝试在此处调用 Promise.resolve(),会导致无限递归。
})

上面示例中,p6 的状态取决于 p5 的状态。

3. 参数是一个 thenable 对象

thenable 对象,是指具有 then 方法的对象。例如:

const obj = {
  then: function(resolve, reject) {
    resolve('foo')
  }
}

上面示例中,obj 对象就是一个 thenable 对象。Promise.resolve() 方法会将这个 thenable 对象转为 Promise 对象,然后就立即执行 thenable 对象的 then() 方法。

const obj = {
  then: function (resolve, reject) {
    console.log(2)
    resolve('foo')
    // reject('foo') // 如果是这样,最终 promise 对象将会变成了 rejected 状态。
  }
}
const promise = Promise.resolve(obj)

promise.then(res => {
  console.log(3)
  console.log(res) // "foo"
})

console.log(1)
// 打印结果分别是 1、2、3、"foo"

// promise 结果:
// {
//   [[Prototype]]: Promise,
//   [[PromiseState]]: "fulfilled",
//   [[PromiseResult]]: "foo"
// }

上述示例中,obj 对象的 then() 方法执行后,对象 promise 的状态变成了 fulfilled,接着执行最后的那个 promise.then() 方法,打印出 "foo"

4. 参数是一个不具有 then() 方法的对象,或者压根不是一个对象,而是原始值。

如果是这种情况,Promise.resolve() 方法返回一个新的 Promise 对象,状态为 fulfilled,且该实例对象的值,就是该参数值。

const p1 = Promise.resolve('foo')
const p2 = Promise.resolve({})

// p1 结果:
// {
//   [[Prototype]]: Promise,
//   [[PromiseState]]: "fulfilled",
//   [[PromiseResult]]: "foo"
// }

// p2 结果:
// {
//   [[Prototype]]: Promise,
//   [[PromiseState]]: "fulfilled",
//   [[PromiseResult]]: {}
// }

在实际项目中,一般是第 4 种情况居多,我似乎真的没见过前三种情况的。

二、Promise.reject()

Promise.reject() 方法会返回一个新的 Promise 实例对象,该实例的状态总是为 rejected

const promise = Promise.reject('foo')

// 相当于
const promise = new Promise((resolve, reject) => reject('foo'))

Promise.resolve() 不同的是,Promise.reject() 方法的参数(无论是原始值、普通对象、还是 Promise 实例对象),将会原封不动地作为返回实例对象的值。

Promise.reject('Oops').catch(err => {
  console.log(err === 'Oops') // true
  // do something...
})

三、Promise.all()

Promise.all() 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

const promise = Promise.all([p1, p2, p3])

上面代码中,Promise.all() 方法接受一个数组作为参数,其中 p1p2p3 都是 Promise 实例。如果数组中包含非 Promise 实例,它们会使用 Promise.resolve() 的方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all() 方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

其中 promise 的状态由 p1p2p3 决定,分为两种情况。

看个例子:

const userIds = [1, 3, 5]
const promiseArr = userIds.map(id => {
  return window.fetch(`/config/user/${id}`) // 假设是请求用户配置
})

Promise
  .all(promiseArr)
  .then(res => {
    // res 是一个数组,每一项对应每个实例的值,即 [[PromiseResult]]
    // 常见做法是将 res 进行解构,即 Promise.all(promiseArr).then(([a, b, c]) => { /* do something... */ })
    // 假设 promiseArr 是一个空的可迭代对象,例如空数组,Promise.all([]) 实例状态为 fulfilled,值为 []。
    // do something...
  })
  .catch(err => {
    // err 为 Promise.all() 被 rejected 的原因(reason)
  })

上面的示例中,promiseArr 是包含 3 个 Promise 实例的数组,只有这 3 个实例的状态都变成 fulfilled,或其中一个变为 rejected,才会调用 Promise.all() 方法的回调函数。

四、Promise.race()

Promise.race() 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

const promise = Promise.race([p1, p2, p3])

Promise.race() 方法同样接受一个可迭代对象,只要 p1p2p3 中有一个实例率先改变状态(fulfiledrejected),promise 的状态就会跟着改变,而且 promise 实例的值就是率先改变的实例的返回值。若可迭代对象中的某一项不是 Promise 实例,仍会使用 Promise.resolve() 进行转换。

当传递一个空的可迭代对象,那么 Promise.race() 实例的状态将会一直停留在 pending。这点跟 Promise.all() 是不同的。

const p1 = Promise.all([])
const p2 = Promise.race([])

setTimeout(() => {
  console.log(p1) // Promise {<fulfilled>: Array(0)}
  console.log(p2) // Promise {<pending>}
})

五、Promise.allSettled()

Promise.allSettled() 是 ES11 标准引入的一个方法,同样还是将多个 Promise 实例包装成一个新的 Promise 实例。只有等所有实例都返回结果(无论是 fulfilledrejected),包装实例的状态才会发生变化。

我认为,这算是对 Promise.all() 存在 rejected 实例情况的一种补全吧。

注意,Promise.allSettled() 的状态,只可能是 pendingfulfilled 状态,不可能存在 rejected 状态。即只会从 pending -> fulfilled 的变化。

我们来看看以下示例,各种情况的结果吧:

const p1 = Promise.reject(1)
const p2 = Promise.resolve(2)
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => { reject(3) }, 1000)
})
const p4 = new Promise(() => { }) // p4 状态将会一直停留在 pending

const p5 = Promise.allSettled([]) // 参数为空迭代对象
const p6 = Promise.allSettled([p4])
const p7 = Promise.allSettled([p1, p2, p3])

setTimeout(() => {
  console.log('p1:', p1)
  console.log('p2:', p2)
  console.log('p3:', p3)
  console.log('p4:', p4)
  console.log('p5:', p5)
  console.log('p6:', p6)
  console.log('p7:', p7)

  p5.then(res => {
    console.log('p5 then:', res)
  })
  p6.then(res => {
    // 这里将不会执行,因为 p6 一直处于 pending 状态
    console.log('p6 then:', res)
  })
  p7.then(res => {
    console.log('p7 then:', res)
  })
}, 2000)

列举以上示例,是为了得出以下结论:

有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,使用 Promise.allSettled() 方法就很有用了。而 Promise.all() 是没办法确保这一点的。

六、Promise.any()

在 ES12 标准中,引入了 Promise.any() 方法,它用于将多个 Promise 实例,包装成一个新的 Promise 实例。

Promise.any() 接受一个 Promise 可迭代对象,只要参数实例中有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态,其值就是参数实例的值。

Promise.any()Promise.race() 很像,只有一个不同点,就是 Promise.any() 不会因为某个参数 Promise 实例变成 rejected 状态而接受,必须要等到所有参数实例的状态都变为 rejected,包装实例的状态才会是 rejected

const p1 = Promise.reject(1)
const p2 = Promise.resolve(2)
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => { reject(3) }, 1000)
})
const p4 = new Promise(() => { }) // p4 状态将会一直停留在 pending

const p5 = Promise.any([]) // p5 会变成 rejected 状态
const p6 = Promise.any([p4])
const p7 = Promise.any([p1, p2, p3])
const p8 = Promise.any([p1, p3])

setTimeout(() => {
  console.log('p1:', p1)
  console.log('p2:', p2)
  console.log('p3:', p3)
  console.log('p4:', p4)
  console.log('p5:', p5)
  console.log('p6:', p6)
  console.log('p7:', p7)
  p5.then(res => {
    console.log('p5 then:', res)
  }).catch(err => {
    // p5 的状态会变成 rejected,因此会执行到这里。
    console.log('p5 catch:', err)
  })
  p6.then(res => {
    // p6 的状态一直会是 pending,因此不会执行回调。
    console.log('p6 then:', res)
  })
  p7.then(res => {
    console.log('p7 then:', res)
  })
  p8.then(res => {
    console.log('p8 then:', res)
  }).catch(err => {
    // 注意 err 是一个对象
    console.log('p8 catch:', err)
    console.dir(err)
  })
}, 2000)

Promise.any() 返回的实例变成 rejected 时,其实例的值是 AggregateError 实例。但传递一个空的迭代对象,Promise.any() 包装实例也会变成 rejected 状态,如 p5

七、总结

关于 Promise.all()Promise.race()Promise.allSettled()Promise.any() 方法,总结以下特点。

  • 它们的用处都是将多个 Promise 实例,包装成一个新的 Promise 实例。

  • 它们都接受一个具有 Iterator 接口的可迭代对象,通常为数组。且会返回一个新的 Promise 实例对象。

  • 它们处理参数为空的可迭代对象的方式不一样,本来就是要处理多个 Promise 对象,才会用到它们,所以这种情况无需理会。真遇到再回来翻阅文档即可,现在我写到这里都记不太清楚其中的区别了,但问题不大。

  • Promise.all() 当所有实例均为 fulfilled 状态,最终的包装实例才会是 fulfilled,其值是一个数组。否则将会是 rejected 状态;

    Promise.race() 则是某个实例的状态发生变化,最终包装实例将对应率先变化实例所对应的值和状态。“发生变化”是指 pending -> fulfilledpending -> rejected

    Promise.allSettled() 单从命名上来猜测,就知道它需要等所有参数实例确定状态后,包装实例的状态才会变成 fulfilled 状态,注意它不存在 rejected 状态的情况。包装实例的返回值是一个数组,数组每项可能是 { status: "fulfilled", value: /* 对应 fulfilled 的值 */ }{ status: "rejected", reason: /* 对应 rejected 的原因 */ },取决于每个参数实例的状态。

    Promise.any() 当某个参数实例的状态变为 fulfilled,那么包装实例就定型了,对应该参数实例的状态和值。否则它必须等到所有参数实例变为 rejected 状态,包装实例的状态才会发生改变,变为 rejected,其值是一个 AggregateError 实例。

The end.