跳转到内容

Promise对象

JavaScript 中异步编程一直是重难点。传统的回调函数虽然解决了问题,但它们复杂、难以维护,且容易引发回调地狱。Promise 对象的出现,以其独特的状态管理和链式调用,为异步编程带来了革命性的改变。阮一峰老师将深入探讨 Promise 的基本概念、使用方法以及其在现代 JavaScript 开发中的重要性,带你掌握异步编程的新范式。

含义

Promise 是异步编程的一种解决方案,比传统的回调函数更合理和强大。Promise 是一个容器,存放着未来会发生的事件(例如回调函数),从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。它的出现大大简化了异步编程。

Promise 对象有两个特点:

  1. 对象状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
  2. 状态一旦发生就不会再改变。对 Promise 对象而言,状态只有从 pending 变为 fulfilled 和从 pending 变为 rejected,只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型),后续随时都能监听到这个状态。事件 Event 与它不同,错过了就不再能监听到。

Promise 也有一些缺点:

  1. 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
  2. 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
  3. 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

基本用法

Promise 对象是一个构造函数,用于生成 Promise 实例。它接受一个函数作为参数,该函数的两个参数分别是 resolvereject,由 JavaScript 引擎提供,不用自己部署。

Promise 接受一个函数作为参数,该函数有两个参数 resolverejectresolve 的作用是 Promise 对象状态从 pending 变为 resolvedreject 的作用是把状态从 pending 变为 rejected

js
new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
})

Promise 实例生成后,可以用 .then 方法分别指定 resolvereject 状态的回调函数,接受两个回调函数作为参数。第一个回调函数是 Promise 对象状态变为 resolved 时调用,第二个回调函数是 Promise 对象状态变为 rejected 时调用。这两个函数都接受 Promise 传递的值作为参数。其中,第二个参数是可选的。

js
new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
}).then(function(value) {
  // success
}, function(error) {
  // failure
});

Promise 对象新建后会立即执行,且调用 resolvereject 不会终止 Promise 的参数函数执行。.then 方法指定的回调函数会在当前脚本所有 同步任务 执行完才会执行。

js
let promise = new Promise(function(resolve, reject) {
  console.log('Promise start');
  resolve();
  console.log('Promise end');
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise start
// Promise end
// Hi!
// resolved.

一般的,调用 resolve 或者 reject 后,Promise 的使命就完成了,后继操作应该放到 then 方法里面,而不应该直接写在 resolvereject 的后面。所以,最好在它们前面加上 return 语句,这样就不会有意外。

Promise.prototype.then()

Promise 原型对象上具有 .then 方法,作用是为 Promise 实例添加状态改变时的回调函数。返回的是一个新的 Promise 实例,因此可以采用链式写法。

js
new Promise((resolve, reject) => {
  resolve(1);
}).then((value) => {
  console.log(value);
  return value * 2;
}).then((value) => {
  console.log(value);
  return value * 2;
}).then((value) => {
  console.log(value);
  return value * 2;
})

Promise.prototype.catch()

Promise.prototype.catch 方法是 .then(null, rejection) 的别名,用于指定发生错误时的回调函数。当 Promise 对象状态变为 reject.then 方法运行中抛出错误时,会调用 catch 方法指定的回调函数。

js
new Promise((resolve, reject) => {
  reject('error');
}).catch((error) => {
  console.log(error);
})

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个 catch 捕获。一般来说,不要在 then 方法里面定义 reject 回调函数,应该使用 catch 方法捕获错误 ❌。

js
new Promise((resolve, reject) => {
  resolve();
}).then((value) => {
  // some ode
}).then((value) => {
  // some ode
}).catch((error) => {
  console.log(error);
})

try...catch 语句不同的是,如果没有使用 catch 方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。只有 Promise 指定在下一轮 事件循环 再抛出错误,才会冒泡到外层。

js
// 不冒泡
new Promise((resolve, reject) => {
  return x + 1
})
console.log('everything is ok') // 正常打印

// 冒泡
new Promise((resolve, reject) => {
  resolve(1)
  setTimeout(() => { throw new Error('error') }, 0)
}).catch((error) => {
  console.log(error)
})

Promose.all()

Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。其参数一般接受一个数组,如果不是数组则要求必须具有 Iterator 接口,且每一项都必须是 Promise 实例。如果不是,则会调用 Promise.resolve 方法,将参数转为 Promise 实例,再进一步处理。

Promise.all 方法返回的新的 Promise 实例,该实例的状态由数组内每一项 PRomise 决定,分为两种情况:

  1. 只有当参数中的所有 Promise 实例都变为 fulfilled 状态,才会变为 fulfilled 状态
  2. 只要有一个变为 rejected 状态,就会变为 rejected 状态,第一个变为 rejected 状态的实例的返回值,会传递给 Promise.all 方法返回的新实例的回调函数

⚠ 注意

如果数组内的 Promise 实例,有自己的 .catch 方法,那么它们抛出的错误不会传递到 Promise.all 方法上,会由自身 .catch 方法捕获;如果没有,则会传递到 Promise.all 方法上。

js
const p1 = new Promise((resolve, reject) => {
  reject('p1 error');
})

const p2 = new Promise((resolve, reject) => {
  reject('error');
}).catch((error) => {
  console.log('p2 error') // p2 error
})

Promise.all([p1, p2]).then((value) => {
  console.log(value);
}).catch((error) => {
  console.log(error) // p1 error
})

Promise.race()

Promise.race 方法与 Promise.all 方法相同,同样是将多个 Promise 实例,包装成一个新的 Promise 实例。每个参数都是 Promise 实例,如果不是则调用 Promise.resolve 转换。

不同的是,只要有一个 Promise 实例状态变为 fulfilledrejected,新的 Promise 实例就会变为 fulfilledrejected 状态,并返回第一个变为 fulfilledrejected 状态的实例的返回值。

js
Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]).then(response => {
  console.log(response)
}).catch(error => {
  console.log(error)
})

Promise.resolve()

Promise.resolve 方法用于将现有对象转为 Promise 对象,Promise.resolve 方法等价于下方这种写法:

js
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

参数分为四种情况:

  1. 参数是一个 Promise 实例,则返回该实例
  2. 参数是一个具有 .then 方法的对象,则把该方法转为 Promise 对象,然后立即执行 .then 方法
  3. 参数不是具有 .then 方法的对象或根本不是对象,则返回一个 fulfilled 状态的 Promise 对象,并且返回该参数
  4. 不带任何参数,则直接返回一个 fulfilled 状态的 Promise 对象

Promise.reject()

Promise.reject 方法返回一个 rejected 状态的 Promise 实例,并将给定的参数作为 rejected 状态的理由,返回的 Promise 实例的回调函数会立即执行。

js
Promise.reject('出错了')
// 等价于
new Promise((resolve, reject) => reject('出错了'))

⚠ 注意

Promise.reject 方法与 Promise.resolve 方法不同的是,Promise.reject 方法的参数会原封不动作为 reject 状态的理由变成后续方法的参数。

js
let thenable = {
  then(resolve, reject) {
    reject('出错了');
  }
}
Promise.reject(thenable).catch(e => {
  console.log(e === thenable) // true
})

Promise.try()

有一种情况:想要把某个函数放到 Promise 内执行,这样就能在 .then 内执行后续的回调函数,.catch 执行报错后处理。但是不确定该函数是同步函数还是异步函数。

一般的,写法如下所示:

js
Promise.resolve().then(fn)

但是这么写有一个缺点,如果 fn 是一个同步函数,那么它会在本轮事件循环最后执行。

js
Promise.resolve().then(() => console.log('promise fn'))
console.log('console')

// console
// promise fn

想要同步函数同步执行,异步函数异步执行,有两个解决方法:

  1. async

    js
     (async () =>{ fn() })().then().catch()

    通过立即执行函数,将 async 函数包裹起来,立即执行里面的 async 函数,如果是同步的就同步执行;如果是异步的则 .then 执行后续。最后通过 .catch 捕获错误

  2. new Promise

    js
     (
       () => new Promise((resolve) => {
         resolve(fn())
       })
     )

    通过 new Promise 将函数包裹起来,如果是同步的则同步执行,如果是异步的则 .then 执行后续。最后通过 .catch 捕获错误

基于此,目前有一个提案提供了 PRomise.try() 方法代替上面的写法。

js
const fn = () => console.log('fn')
Promise.try(fn)

Promise.try() 方法可以用 Promise.catch 统一捕获所有同步或异步的报错。

部署有用的附加方法

done()

Promise 内部错误不会冒泡,如果最后一个 .catch 内部还抛出错误,则无法捕捉到。为此可以在 Promise 原型上挂一个 done 方法,作用是保证抛出任何有可能出现的错误

js
asyncFunc()
  .then(f1)
  .catch(r1)
  .then(f2)
  .done()

实现代码如下:

js
Promise.prototype.done = function (onFulfilled, onRejected) {
  this.then(onFulfilled, onRejected)
    .catch(function (reason) {
      // 抛出一个全局错误
      setTimeout(() => { throw reason }, 0)
    })
}

finally()

finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

实现代码如下:

js
Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

总结

Promise对象是一种用于异步编程的解决方案,能够简化异步操作的处理。

Promise对象有三个状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败),一旦状态改变,就不会再变,称为resolved(已定型)。

Promise 的构造函数接受一个函数,该函数有两个参数 resolvereject,分别用于改变状态。Promise 实例可以通过 .then 方法添加回调函数,处理成功或失败的情况,并通过链式调用实现复杂的异步流程。Promise.prototype.catch 方法用于捕获错误,而Promise.allPromise.race方法用于处理多个 Promise 实例。

Promise.resolvePromise.reject 方法用于将值或错误转化为 Promise 对象,Promise.try 方法用于统一处理同步和异步函数的执行。donefinally 方法用于确保错误被捕获和在 Promise 完成后执行某些操作。