likes
comments
collection
share

对 js 中 Promise 的细节探究,顺便纠正了 MDN 的一个小错误

作者站长头像
站长
· 阅读数 15

在平常项目中,我们经常会使用到 promise,比如处理接口请求的时候,一般会用到的 axios,就是一个基于 promise 的网络请求库。看似简单的 promise 背后,其实也隐藏着诸多你知道或者不知道的细节,本文就是对它们的一个总结。

缘起

在 ES6 之前,官方规范里还没有 Promise 对象那会,如果我们想封装一个请求接口的方法,可能会是这样:

// 例 1
function request(succeedCallBack, failedCallBack) {
  setTimeout(() => {
    if (Math.round(10 * Math.random()) % 2) {
      succeedCallBack('请求成功')
    } else {
      failedCallBack('请求失败')
    }
  }, 1000)
}

// 发送请求
request(
  res => console.log(res),
  err => console.log(err)
)

我们用一个定时器来模拟对接口的请求,用 Math.round(10 * Math.random()) % 2 的结果(0 或 1)来模拟请求的成功与失败。为了能在第 13 行发送请求后获取到请求结果,我们需要传入两个回调函数,在请求成功时执行 succeedCallBack(),在请求失败时执行 failedCallBack()。这样做似乎也没什么问题,但有 2 个不太方便的点:

  1. 我们定义 request 函数时还得费点脑细胞考虑该怎么设计回调函数的使用和名称;
  2. 如果别人也想调用 request,他必须来看看我这个函数是怎么定义的,他才知道该怎么用。

这就增加了我们的开发成本。从 ES6 开始,我们可以按照官方的规范,直接使用 Promise 对象,来封装一个异步操作并可以获取其结果,从而解决了上述的 2 点麻烦:

// 例 1.1
function request() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.round(10 * Math.random()) % 2) {
        resolve('请求成功')
      } else {
        reject('请求失败')
      }
    }, 1000)
  })
}

request()
  .then(value => console.log(value))
  .catch(reason => console.log(reason))

下面就来说说 promise 的一些细节问题。

executor

Promise 是个类,我们通过关键字 new ,并且传入一个处理器函数(executor function)来创建 Promise 实例对象。executor 本身是个回调函数,是个同步回调,就像数组遍历相关的回调函数那样,是立即执行的,等完全执行完了才会结束,不会放入回调队列中。executor 接收的参数也是两个回调函数 —— resolvereject,它们都是异步回调函数,不会立即执行,而是放入回调队列中将来执行。

Promise 的三种状态

Promise 对象有 3 种状态,且 promise 必然处于它们中的某一种状态:

  • 当 executor 执行时,promise 处于 pending 状态;
  • 如果在 executor 中执行的是 resolve(),promise 就会变为 fulfilled 状态,表示异步操作成功完成。接着就会调用.then() 方法的 onFulfilled 回调函数;
  • 如果在 executor 中执行的是 reject(),promise 就会变为 rejected 状态,表示异步操作完成失败。接着就会调用 .then() 方法的 onRejected 回调函数或者是 .catch() 方法。

一个 Promise 对象的状态只能改变一次,也就是说一旦调用了 resolve()reject()之后,状态就被锁定(settled),之后再次调用 reject()resolve() 是不会改变 promise 状态的。下面放一张引用自 MDN 的图来说明 Promise 对象的基本流程 : 对 js 中 Promise 的细节探究,顺便纠正了 MDN 的一个小错误

resolve 的参数

1. 普通的值或对象

一般情况下,传给 resolve() 的参数(value)都是一个普通的值或对象,然后触发 .then(onFulfillment),并且将该 value 传递给 then 方法。比如直接执行下面的例 2,将打印得到“成功了 我是返回的数据”:

// 例 2
new Promise((resolve, reject) => {
  resolve({ msg: '我是返回的数据' })
}).then(
  value => console.log('成功了', value.msg),
  reason => console.log('失败了', reason)
)

但其实 resolve() 的参数还可以是其它的情况。

2. Promise 对象

如果传给 resolve() 的又是一个 Promise 对象:

// 例 2.1
new Promise((resolve, reject) => {
  resolve(new Promise((resolve, reject) => {}))
}).then(
  value => console.log('成功了', value.msg),
  reason => console.log('失败了', reason)
)

此时外面这个 promise 的状态,将由这个传给 resolve 的 promise 的状态决定。比如例 2.1 我并没有在第 3 行的这个作为 resolve 参数的 promise 的 executor 中执行 resolve()reject(),所以里面这个 promise 的状态为 pending,故而外面这个 promise 的状态也为 pending,执行例 2.1 不会有任何打印。

如果让传给 resolve 的 promise 的状态变为 rejected,那么外面这个 promise 的状态也会变为 rejected ,接下去调用的会是 .then(onRejection),并且传递给 then 方法的值将会是第 5 行传给 reject() 的内容,即执行例 2.1.1 将打印“失败了 我是失败的理由”。

// 例 2.1.1
new Promise((resolve, reject) => {
  resolve(
    new Promise((resolve, reject) => {
      reject('我是失败的理由')
    })
  )
}).then(
  value => console.log('成功了', value.msg),
  reason => console.log('失败了', reason)
)

3. thenable

如果传给 resolve() 的是一个 thenable,即一个带有 then 方法的对象。那么这个 then 方法会被执行,并且可以认为 resolvereject 两个方法会传给该 then 方法,再根据该 then 方法的执行的是 resolve 或 reject 决定外面这个 promise 的状态:

// 例 2.1.2
new Promise((resolve, reject) => {
  resolve({
    then(resolve, reject) {
      resolve('我是成功信息')
    }
  })
}).then(
  value => console.log('成功了', value), // 成功了 我是成功信息
  reason => console.log('失败了', reason)
)

执行例 2.1.2 将打印“成功了 我是成功信息”,可见第 9 行的 value 为传给第 5 行这个 resolve() 的值,而不是传给第 3 行这个 resolve() 的 thenable。事实上,then 方法的执行返回的还是一个 promise,所以本质上传给 resolve 的参数为 thenable 的情况和为 Promise 对象的情况是一样的。

实例方法

所谓实例方法指的是这些方法都是定义在 Promise 的原型对象 Promise.prototype 上的方法。

then

参数

在项目里我一般习惯只传给 then 方法一个当 promise 的状态变成 fulfilled 时会调用的回调函数 onFulfilled,然后用 catch 方法去处理 promise 被 rejected 的情况。但其实根据 Promises/A+ 规范,是没有 catch 方法的,promise 变为 rejected 后要调用回调应该是 then 方法的第二个参数 onRejected:

// 例 3
new Promise((resolve, reject) => {
  // ...
}).then(
  value => console.log('成功了', value), // onFulfilled
  reason => console.log('失败了', reason) // onRejected
)

同一个 promise 可以被多次调用 then 方法

比如下面的例 3.1,我们定义了 2 次 p.then(),由于第 3 行执行了 resolve(), p 的状态变为 fulfilled,2 个 p.then()的 onFulfilled 回调都会执行:

// 例 3.1
const p = new Promise((resolve, reject) => {
  resolve()
})

p.then(
  value => console.log('成功了1', value), // 成功了1 undefined
  reason => console.log('失败了1', reason)
)

p.then(
  value => console.log('成功了2', value), // 成功了2 undefined
  reason => console.log('失败了2', reason)
)

传入 then 方法的回调函数的返回值

then 方法本身有个返回值,为一个新的 Promise 对象,所以可以再次链式调用 then 方法:

// 例 3.2
const p = new Promise((resolve, reject) => {
  resolve()
})
p.then(() => {
  return 'Jay'
}).then(value => console.log(value)) // Jay

例 3.2 中第 7 行打印结果之所以为“Jay”,与传入 then 方法的参数,也就是回调函数的返回值有关:

1. 普通值

如果返回值像例 3.2 中第 6 行那样是个普通的值,或者是个普通的对象,则第 5 行这个 then 方法返回的 promise 的状态就会变为 fulfilled,并且相当于会将返回的值作为参数传给新 promise 的 resolve。then 方法的实现相当于下面例 3.2.1 这样:

// 例 3.2.1
function then() {
  return new Promise((resolve, reject) => {
    resolve('Jay')
  })
}

所以例 3.2 第 7 行链式调用的 then 方法,里面拿到的 value 值实际上相当于例 3.2.1 中的这个 promise 里传给 resolve 的值。

2. promise

如果返回值是一个 promise,那么就相当于传给例 3.2.1 第 4 行 resolve() 的值是一个 promise,那么此时的情况就和前面提到的 executor 中 resolve 参数的情况分析一样了。比如我们 return 的 promise 执行的是 reject(),那么在之后链式调用的 then 方法里就会在第二个参数 onRejected 回调中获取到 reason:

// 例 3.2.2
p.then(() => {
  return new Promise((resolve, reject) => {
    reject('Zhou')
  })
}).then(
  value => console.log('value', value),
  reason => console.log('reason', reason) // reason Zhou
)

3. thenable

如果返回的是一个 thenable,相当于给例 3.2.1 的 resolve 传入了 thenable,那么道理就和对 resolve 的参数分析的第 3 点是一样的:

// 例 3.2.3
p.then(() => {
  return {
    then(resolve, reject) {
      resolve('亦黑迷失')
    }
  }
}).then(
  value => console.log('value', value), // value 亦黑迷失
  reason => console.log('reason', reason)
)

上面举的例子都是最开始这个 promise 的状态变为 fulfilled, 执行了 then 方法的 onFulfilled 回调。如果例 3.1 第 3 行执行的是 reject(),即 promise 状态为 rejected,then 方法的回调执行的是 onRejected,那么其返回值的不同情况同样适用于上面的分析。比如下面的例 3.3,第 6 行返回了一个普通值,那么链式调用时会执行的是第 8 行的代码:

// 例 3.3
const p = new Promise((resolve, reject) => {
  reject('错误原因')
})
p.then(undefined, reason => {
  return 'Jay'
}).then(
  value => console.log('value', value), // value Jay
  reason => console.log('reason', reason)
)

catch

虽然在 Promises/A+ 规范中,关于拒绝(rejected )或错误状态的捕获回调函数,应该作为 then 方法的第二个参数。但是 ES6 的 Promise 实现了 catch 方法,使得代码书写起来更加清晰:

// 例 4
const p = new Promise((resolve, reject) => {
  reject('错误原因')
})
p.then(undefined, reason => console.log(reason)) // 错误原因
p.catch(reason => console.log(reason)) // 错误原因

例 4 中的第 6 行使用 catch 来处理拒绝的情况,可以看成是第 5 行这种写法的语法糖。 如果在例 4 的第 3 行不是执行 reject 而是抛出了错误,同样是会执行 onRejected 回调:

// 例 4.1
const p = new Promise((resolve, reject) => {
  throw new Error('抛出了错误')
})

p.catch(reason => {
  console.log(reason)
  console.log('之后的代码依旧可以执行')
})

在控制台可以看到错误打印信息:

对 js 中 Promise 的细节探究,顺便纠正了 MDN 的一个小错误

因为只是打印了错误信息而不是报错,所以不会影响第 8 行代码的执行。 我们前面说过了,then 方法本身会返回一个新的 Promise 对象,但是像例 4.2 第 6 行这样的链式调用,catch 捕获的依然是 p 的拒绝:

// 例 4.2
const p = new Promise((resolve, reject) => {
  reject('拒绝了')
})

p.then(value => console.log(value)).catch(reason => console.log(reason)) // 拒绝了

若 p 的状态变为 fulfilled,那么例 4.2 中的 catch 就会去捕获 then 方法返回的 promise 的拒绝或错误,如例 4.2.1:

// 例 4.2.1
const p = new Promise((resolve, reject) => {
  resolve('Jay')
})

p.then(value => {
  console.log(value) // Jay
  return Promise.reject('拒绝了')
}).catch(reason => console.log(reason)) // 拒绝了

返回值

同 then 方法一样,catch 方法本身也是返回一个新的 Promise 对象,该 promise 的状态与传入 catch 的 onRejected 回调的返回值有关,判断方式与 then 方法的一样。下面的例 4.3 中,第 8 行返回的是一个字符串,那么 catch 返回的这个 promise 就会变为 fulfilled,所以会被调用的是第 10 行的 then 方法,而不是第 11 行的 catch:

// 例 4.3
new Promise((resolve, reject) => {
  reject('拒绝')
})
  .then(value => console.log(value))
  .catch(reason => {
    console.log(reason) // 拒绝
    return '我是 catch 的参数 onRejected 回调函数的返回值'
  })
  .then(value => console.log('value', value)) // value 我是 catch 的参数 onRejected 回调函数的返回值
  .catch(reason => console.log(reason))

finally

finally 方法比较简单,是 ES9(ES2018)添加的特性,在项目中我一般是在该方法的参数 onFinally 回调中写结束请求 loading 的语句。 onFinally 回调函数不接收参数。

类方法

Promise 除了有上面介绍的 3 种实例方法,还有 6 个类方法,也就是如果用 class 语法定义 Promise 时,用 static 关键字定义的方法。比如 Promise.resolve()Promise.reject()Promise.all()Promise.allSettled() 等,都比较简单,就不多说了,仅就 ES12(ES2021)新增的 Promise.any() 方法做一些说明。

Promise.any()

如果我们想得到的是第一个变为 fulfilled 的 promise,忽略掉在它之前的为 rejected 的 promise,就可以使用 Promise.any() 方法:

// 例 5.5
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Jay')
  }, 2000)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('拒绝')
  }, 1000)
})

Promise.any([p1, p2])
  .then(value => console.log('value', value)) // value Jay
  .catch(reason => console.log('reason', reason))

如果传入 Promise.any()迭代器中每个 promise 的结果均为 rejected,那么其返回的 promise 就会变为 rejected,并且返回一个 ES12 新增的 Error 的子类 AggregateError 类型的实例,其有个 errors 属性,属性值是由所有失败值填充的数组:

// 例 5.5.1
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('拒绝1')
  }, 2000)
})
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('拒绝2')
  }, 1000)
})

Promise.any([p1, p2])
  .then(value => console.log('value', value))
  .catch(reason => {
    console.log(reason) // AggregateError: All promises were rejected
    console.log(reason.errors) // ['拒绝1', '拒绝2']
  })

请注意,目前 Promise.any() 方法依然是实验性的,尚未被所有的浏览器完全支持。另外,当时我去看 MDN 文档查看 Promise.any() 的内容,它说 AggregateError 有个 error 属性:

对 js 中 Promise 的细节探究,顺便纠正了 MDN 的一个小错误

但是其实应该是 errors , 英文版就是正确的:

对 js 中 Promise 的细节探究,顺便纠正了 MDN 的一个小错误

于是提交了个 pull request,现在已经改过来了:

对 js 中 Promise 的细节探究,顺便纠正了 MDN 的一个小错误

对 js 中 Promise 的细节探究,顺便纠正了 MDN 的一个小错误 对 js 中 Promise 的细节探究,顺便纠正了 MDN 的一个小错误