Promise 对象

259 阅读10分钟

一、Promise 探究

先来打印 Promise ,看一下它的结构组成。

image.png

1. Promise 是什么

由上可见,Promise 是一个构造函数,它有 reject、resolve、all、race 等静态方法,原型上有 then、catch 等方法,所以 new 的实例对象肯定有 then、catch 这些方法。

2. 先来 new 一个?

let p = new Promise(function(resolve, reject) {
  // 做一些异步操作
  setTimeout(() => {
    console.log('执行完成');
    resolve('随便什么数据');
  }, 1000);
});

// 1s 后,输出'执行完成'

Promise 的参数是一个函数,该函数会传入两个参数resolve、reject,可以将 Promise 状态变为 fulfilledrejected

在上面的代码中,我们执行了一个异步操作 setTimeout,然后 1s 后,输出“执行完成”,并调用了 resolve方法。注意:虽然仅仅是 new 了一个对象,并没有调用参数函数,但该函数已经执行了。即 「new Promise() 和 执行参数函数 是同步的」,所以我们在用 Promise 的时候一般是外面再包一层函数,需要的时候再去调用,如:

function runAsync() {
  let p = new Promise(function(resolve, reject) {
    // 做一些异步操作
    setTimeout(() => {
      console.log('执行完成');
      resolve('随便什么数据');
    }, 1000);
  });
  return p;  // 返回 promise实例
}
runAsync();  // 运行外部函数,间接执行 Promise

3. Promise.prototype.then

在执行 runAsync函数 后,我们会得到一个 Promise实例,实例上肯定有 then、catch 方法。

runAsync().then(function(data) {
  console.log(data);
});
// 1s 后输出 “执行完成”、紧接着输出 “随便什么数据”

在返回的 promise 实例上直接调用 then 方法。then 方法接收的第一个参数是函数,该函数可以拿到我们在 runAsync 函数中调用resolve时传的参数

4. Promise 和回调函数的对应关系

如果 函数f1 是异步操作并且 函数f2 又必须等到 f1 执行完成,那么我们之前采用的是传统形式的回调函数 f1(f2)。(f2是回调函数)

而对于 Promise 来说,我们将异步操作放到了 Promise 参数中。等到异步操作结束后,通过 resolve 或 reject 来改变Promise对象的状态,之后依据状态去执行 then 里的回调函数(或catch里的回调函数)。

  • 异步操作f1把异步操作放到了 new Promise() 的参数函数中
  • 回调函数f2then 里面的函数
function runAsync() {
  let p = new Promise(function(resolve, reject) {
    // 做一些异步操作
    setTimeout(() => {
      console.log('执行完成');
      resolve('随便什么数据');
    }, 1000);
  });
  return p;  
}
runAsync().then(function(data) {
  console.log(data);
});

5. 为什么要用 Promise:链式调用

我们可以看到,Promise 其实就是把传统形式的回调函数分离出来,在异步操作执行完后,用链式调用的方式来执行回调函数。像上面这个函数,我们也可以用传统回调去写,如下:

function runAsync(callback) {
  setTimeout(() => {
    console.log('执行完成');
    callback('随便什么数据');
  }, 1000);
}
runAsync(function(data) {
  console.log(data);
});

但是当出现多层回调的时候,或者当回调函数也是一个异步操作,并且执行完后也有对应的回调函数的时候,我们怎么处理呢?总不能定义多个callback传进去吧,这样会使得程序结构混乱、流程难以追踪。而 Promise 的优势就在于,它可以then 方法中返回下一个异步操作的 promise实例,然后继续调用 then 来执行回调函数

runAsync1()
.then(function(data) {
  console.log(data);
  return runAsync2();
})
.then(function(data) {
  console.log(data);
});
// 1执行完成
// 随便什么数据1
// 2执行完成
// 随便什么数据2

6. then方法的返回值

then 方法不仅可以 return promise实例,还有以下几种情况:

  • return 1 个值,那么 then 返回的promise状态会变为 fulfilled,并将返回值作为回调的参数。
  • 没有任何返回值,那么 then 返回的promise...也是 fulfilled,并且后面回调的参数是 undefined
  • 抛出一个错误,那么...rejected,并将抛出的错误作为回调参数值。
runAsync1()
.then(function(data) {
  console.log(data);
  return runAsync2();
})
.then(function(data) {
  console.log(data);
  return '直接返回数据';
})
.then(function(data) {
  console.log(data);
});
// 1执行完成
// 随便什么数据1
// 2执行完成
// 随便什么数据2
// 直接返回数据

7. catch

catch 方法用来指定 reject 的回调。

runAsync()
.then(function(data) {
  console.log(data);
})
.catch(function(reason) {
  console.log(reason);
});

catch 还有另外一个作用:在执行 fulfilled 的回调函数时,如果抛出异常,那么并不会报错卡死js,而是会进到后面的 catch 方法中,如下

runAsync()
.then(function(data) {
  console.log(data);
  console.log(x);   // 没有定义x
})
.catch(function(reason) {
  console.log(reason);
});
// 将错误原因传给了reason参数

二、Promise 基础概念

1. Promise 对象的状态

Promise 对象有 3 种状态:pending 异步操作未完成,fulfilled 异步操作成功,rejected 异步操作失败。并且只有 2 种变化的方式:pending -> fulfilledpending -> rejected

通过JS引擎内置的 resolve() 和 reject() 两个函数实现状态的改变(不是Promise的静态方法),如下:

  • 异步操作成功时调用 resolve 函数,Promise实例传回一个值,状态变为fulfilled
  • 异步操作失败时调用 reject 函数,Promise实例抛出一个错误,状态变为 rejected

resolve 函数的参数除了正常的值外,还可能是另一个 promise实例,此时状态取决于参数实例的状态,自己的状态无效了。

let p1 = new Promise(function (resolve, reject) {
  // ...
});
let p2 = new Promise(function (resolve, reject) {
  // ...
  resolve(p1);
});
// p1 的状态决定了 p2 的状态

调用 resolve 或 reject 函数并不会终结 Promise 的参数函数的执行。 但一般来说,调用 resolve 或 reject 后,Promise的使命就完成了,后续操作应该放到 then 方法里面,因此最好在 resolve/reject 函数前面加上 return 语句。

new Promise((resolve, reject) => {
  resolve(1);   // 推荐做法: return resolve(1);
  console.log(2);  
}).then(r => {
  console.log(r);
});
// 2  1

另外,需要牢记的是:

  • 对象的状态不受外界影响,只有异步操作的结果才能决定 Promise 的状态。
  • 一旦状态改变,就不会再变

2. Promise.prototype.then /catch

1. then方法和链式调用

then 方法的作用是为 Promise 实例添加状态改变时的回调函数,两个参数(均可选)分别是 异步操作成功(状态变为 fulfilled)的回调函数,和 异步操作失败(状态变为 rejected)的回调函数,可省略第 2 个参数,他们都接受实例传出的值作为参数

new Promise((resolve, reject) => {
  resolve(1);
})
.then(() => {
  console.log(2);
})
.then(() => console.log(3), (v) => console.log(v));
// 2 3

2. 怎么中断链式调用? 可以在某个异步操作中添加:

  • throw 'break':抛出异常,则后面的Promise新建 和 链式调用 都中断。
  • return new Promise((resolve, reject) => {}) 让实例永远处于 pending状态,中断链式调用。
  • Promise.reject,中断链式调用。

3. catch

catch 方法和 then 的第二个参数一样,用来指定 reject 的回调函数,它实际是 obj.then(undefined, fn) 的语法糖。

因此如下,catch的返回值也像 then 看齐,在没有返回值时,catch 返回的 promise 状态会变为 fulfilled 执行 then 对应的回调,传参值为 undefined。

new Promise((resolve, reject) => {
  reject(1);
})
.catch(() => {
  console.log(2);
})
.then(() => console.log(3), (v) => console.log(v));
// 2 3

运行完 catch方法后,会接着运行后面的then方法指定的回调函数。

4. Promise错误处理

推荐使用 then...catch 的方式来定义回调函数,因为:

  • 用catch 可以捕获前面 then 指定回调的错误

  • Promise对象的错误会一直向后传递,直到被捕获为止。我们用catch 可以捕获前面的多个 promise对象的错误 (catch自身的错误,如果后面没有catch则不能被捕获,前面的catch管不了后面的then)。

  • 如果catch返回的还是promise对象,那么它后面的then方法可以继续执行。当catch前面都没有error时,会跳过catch继续执行后面的then。

  1. 捕获错误的方式(不能用 try ...catch,因为它用于捕获同步操作的异常,try 中如果放异步操作,那么catch是捕获不到的)
  • catch
  • then 的第二个参数

3. all 的用法

all 方法把多个promise实例包装成了一个新的promise实例,其中 all 的参数是一个数组,数组成员都是Promise实例

Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});
// 1执行完成
// 2执行完成
// 3执行完成
// ['随便什么数据1', '随便什么数据2', '随便什么数据3']
let p = Promise.all([p1, p2, p3]);

p的状态由p1、p2、p3决定,

  • 只有p1、p2、p3 的状态都变成 fulfilled,p的状态才会变成 fulfilled。此时all 方法会在所有异步操作执行完后,将这多个异步操作返回的结果放到数组中传给p的回调函数
  • 只要其中任一实例状态变成 rejected,p的状态就会变为 rejected,此时并不会等待所有的异步操作执行完,而是立刻将第一个被 rejected 的实例的返回值,传递给 p 的回调函数执行。

Promise.all 方法提供了并行执行异步操作的能力,有了all,就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据

4. race 的用法

race 和 all 用法一样,但 race 是只要p1、p2、p3 之中有一个实例率先改变状态,那么p的状态就跟着改变,并立刻将该实例的返回值传给p的回调函数。

并不会等待所有异步操作执行完成,同样,p 的回调函数执行也不影响其他异步操作实例执行。

【注意】Promise.allPromise.race 都是并行的。

  • 并行:多个异步请求同时执行。
  • 串行:一个异步请求完成后再进行下一个请求。

5. Promise.resolve / reject:将现有对象转为promise对象

  1. Promise.resolve
Promise.resolve('foo');
等价于
new Promise(resolve => resolve('foo'));

(1)对于不带参数的Promise.resolve()、参数是原始值的Promise.resolve('hello')、不具有then方法的对象,那么Promise.resolve会直接返回一个resolved状态的promise对象,对应的回调函数会作为微任务在本轮事件循环结束时执行。

(2)对于参数就是promise对象的,那么Promise.resolve(p) 不会作任何修改,直接返回该实例。

(3)如参数是一个具有then方法的对象,那么会将这个对象转成Promise对象,然后立即执行该对象中的then方法。then方法执行后,p1的状态就变为resolved,再执行最后的then方法。

  1. Promise.reject 返回一个新的 promise实例,该实例的状态为 rejected

6. 其他方法

  1. 实例方法 finally 不管Promise对象状态如何,都会执行的操作。它的回调函数不接受任何参数。

  2. 静态方法 allSettled 不管每一个异步操作是成功和失败, allSettled 方法都会等所有的异步操作都结束了,再进行下一步操作。参数和返回值的形式类似于 all 方法。并且新的promise实例只要发生状态变更,那么一定是变成 fulfilled

  3. 静态方法 any 和 all 更好相反,只要一个变为 fulfilled,新实例状态就变为 fulfilled;只有当所有的都变成 rejected,那么包装实例才会变成 rejected。

7. 设计思想和优缺点

设计思想:所有的异步操作都返回一个 Promise 实例,然后使用 then 方法指定下一步的回调函数。

优点:避免了层层嵌套的回调函数

缺点:

  • 无法取消 Promise,一旦 new 创建了就会立即执行,无法中途取消。
  • 与 try...catch 不同,如果没有设置catch等处理错误的回调函数,那么Promise抛出的错误,不会反应到外部,也就是没有任何反应
  • 当处于 pending状态时,不能知道目前的进展是刚刚开始还是即将完成。

三、Promise/A+ 规范

参考文章

1. 大白话讲解Promise

2. Promise 初步详解

3. Promise/A+ 规范