# (六)Promise 初探

TIP

这篇文章算是我对 Promise 其中基础部分的理解,因为阮一峰老师的 ES6 基础已经足够好了,本文更像是一个字典,我也会尽量串联起相关的知识点。 注意,本篇文章更像示例解释,下一篇我们一起研究点有意思的,共勉~

# 1.什么是 Promise

  • 是异步编程的一种解决方案
  • 是一个容器,里面保存着某个未来才会结束的事件的结果。
  • 是一个对象,从它可以获取异步操作的消息。

Promise 对象有以下两个特点:

  1. 对象的状态不受外界影响:
    • Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)
    • 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  2. 一旦状态改变,就不会再变:任何时候都可以得到这个结果。
    • Promise 对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected
    • 如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。

优点

  1. Promise 对象,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
  2. 此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

缺点

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

# 2.基本用法

  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供。
  1. resolve 函数的作用是,将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
  2. reject 函数的作用是,将 Promise 对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。

const promise = new Promise(function(resolve, reject) {
  console.log("1");
  resolve();
});

promise.then(function() {
  console.log("2");
});

console.log("3");

// 1
// 3
// 2

上面代码中,打印数字的顺序和我们代码的位置有些出入,看到下面一节,大家就明白了~

# 二、then 和 catch

# 2.1 Promise.prototype.then

then 方法的作用是为 Promise 实例添加状态改变时的回调函数。

  • 第一个参数是 resolved(成功)状态的回调函数
  • 第二个参数是 rejected(失败)状态的回调函数。

then 方法返回的是一个新的 Promise 实例(注意,不是原来那个 Promise 实例)

示例

new Promise((resolve, reject) => {
  return resolve(1);
})
  .then((item) => {
    console.log(item);
    return item + 1;
  })
  .then((item) => {
    console.log(item);
    return item + 1;
  })
  .then((item) => {
    console.log(item);
    return item + 1;
  });

//1
//2
//3

# 2.2、Promise.prototype.catch

catch 方法,等于 then 方法的第二个参数的别名,用于指定发生错误时的回调函数。

  • 如果该对象状态变为resolved,则会调用then()方法指定的回调函数;
  • 如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数;
p.then((val) => console.log("fulfilled:", val)).catch((err) =>
  console.log("rejected", err)
);

// 等同于
p.then((val) => console.log("fulfilled:", val)).then(null, (err) =>
  console.log("rejected:", err)
);

注意,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获;

const promise = new Promise(function(resolve, reject) {
  throw new Error("test");
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

# 3.Promise 内部的错误

# 3.1 Promise 状态已经变成 resolved,再抛出错误是无效的。

因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。

const promise = new Promise(function(resolve, reject) {
  resolve("ok"); // 状态改为完成
  throw new Error("test"); // 抛出错误、但不会被捕获到
});
promise
  .then(function(value) {
    console.log(value); // ok
  })
  .catch(function(error) {
    console.log(error); // 并没有执行,因为没有状态 变为 成功后不会再改变了
  });

上面代码中,Promise 在 resolve 语句后面,再抛出错误,不会被捕获,等于没有抛出。

Promise 对象的错误具有冒泡性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个 catch 语句捕获。

getJSON("/post/1.json")
  .then(function(post) {
    return getJSON(post.commentURL);
  })
  .then(function(comments) {
    // some code
  })
  .catch(function(error) {
    // 可以处理前面三个 Promise 产生的错误
  });

一般来说,不要在 then()方法里面定义 Reject 状态的回调函数(即 then 的第二个参数),总是使用 catch 方法。

# 3.3 Promise 对象内部的错误不会影响代码运行

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为 x 没有声明
    resolve(x + 2);
  });
};

someAsyncThing().then(function() {
  console.log("everything is great");
});

setTimeout(() => {
  console.log(123);
}, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123

可以看到,promise 内部的错误不会影响外面的代码的运行,通俗的说法就是“Promise 会吃掉错误”。

# 3.4 抛出的错误会传递到外层代码

再看下面的例子。

const promise = new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
  setTimeout(() => {
    throw new Error("3");
  }, 1000);
});

promise.then((item) => {
  console.log(item);
});
// 2
// 1
// Uncaught Error: 3

上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。

# 3.4 catch()后面可以继续调用 then()方法

const someAsyncThing = function() {
  return new Promise(function(resolve, reject) {
    // 下面一行会报错,因为 x 没有声明
    resolve(x + 2);
  });
};

someAsyncThing()
  .catch(function(error) {
    console.log("oh no", error);
  })
  .then(function() {
    console.log("carry on");
  });
// oh no [ReferenceError: x is not defined]
// carry on

catch 捕获到 promise 中的报错后,仍然会执行紧接着之后的 then 方法。

# 四、Promise.prototype.finally()

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。看名字就知道它是兜底的~

# 4.1 finally 的特点

  • 总会执行 finally 方法
  • 不接受参数,不依赖 Promise 的执行结果(即它的执行与状态无关)
  • finally 方法总是会返回原来的值。
// 在执行完then或catch的回调后,finally就可以就会执行
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
Promise.prototype.finally = function(callback) {
  // this => promise实例
  let P = this.constructor; // Promsie对象
  const resolve = (value) => {
    // Promise.resolve('123') => p.then(('123')=>{ todo })
    P.resolve(callback()).then(() => value);
  };
  const reject = (reason) => {
    P.resolve(callback()).then(() => {
      throw reason;
    });
  };
  return this.then(resolve, reject);
};

不难看出来,finally 的方法就等于 then 和 catch 方法的集合,还可以看到,finally 方法总是会返回原来的值。

// resolve 的值是 undefined
// 因为没有
Promise.resolve(2).then(
  () => {},
  () => {}
);

// resolve 的值是 2
Promise.resolve(2).finally(() => {});

// reject 的值是 undefined
Promise.reject(3).then(
  () => {},
  () => {}
);

// reject 的值是 3
Promise.reject(3).finally(() => {});

# 5.Promise.all()

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

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

上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3 都是 Promise 实例,如果不是,就会先调用下面讲到的 Promise.resolve 方法,将参数转为 Promise 实例。

p 的状态由数组内每个元素的状态决定,以上面的代码举例:

  • 只有 p1、p2、p3 的状态都变成 成功,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
  • 反之,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

注意,如果作为参数的 Promise 实例,自己定义了 catch 方法,那么它一旦被 rejected,并不会触发 Promise.all()的 catch 方法。

const p1 = new Promise((resolve, reject) => {
  resolve("hello");
})
  .then((result) => result)
  .catch((e) => e);

const p2 = new Promise((resolve, reject) => {
  throw new Error("报错了");
})
  .then((result) => result)
  .catch((e) => e);

Promise.all([p1, p2])
  .then((result) => console.log(result))
  .catch((e) => console.log(e));
// ["hello", Error: 报错了]

分析代码:

  1. p1 实例正常 resolve
  2. p2 首先会 rejected,
    • p2 有自己的 catch 方法,捕获错误并返回了一个新的 Promise 实例,p2 指向的实际上是这个实例。
    • 新实例执行完 catch 方法后,也会变成 resolve
  3. Promise.all()方法参数里面的两个实例都会 resolved,因此会调用 then 方法指定的回调函数,而不会调用 catch 方法指定的回调函数。

# 6.Promise.race()

race,角逐、比赛,顾名思义,它和 all 一样接受多个 Promise 实例、只要有一个实例改变状态,它就会跟着改变

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

上面代码中,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。

Promise.race()方法的参数与 Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的 Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。

const p = Promise.race([
  fetch("/resource-that-may-take-a-while"),
  new Promise(function(resolve, reject) {
    setTimeout(() => reject(new Error("request timeout")), 5000);
  }),
]);
// 5s内请求没有得到返回,第二个实例就会抛出错误了~
p.then(console.log).catch(console.error);

分析代码:

上面代码中,如果 5 秒之内 fetch 方法无法返回结果,变量 p 的状态就会变为 rejected,从而触发 catch 方法指定的回调函数。

# 7.Promise.allSettled()

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是 fulfilled 还是 rejected,包装实例才会结束。

该方法返回的新的 Promise 实例,一旦结束,状态总是 fulfilled,不会变成 rejected。Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入 Promise.allSettled()的 Promise 实例。

const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);

const allSettledPromise = Promise.allSettled([resolved, rejected]);

allSettledPromise.then(function(results) {
  console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]

上面代码中,Promise.allSettled()的返回值 allSettledPromise,状态只可能变成 fulfilled。

它的监听函数接收到的参数是数组 results。该数组的每个成员都是一个对象,对应传入 Promise.allSettled()的两个 Promise 实例。

  • 每个对象都有 status 属性:('fulfilled''rejected'
    • fulfilled 时,对象有 value 属性
    • rejected 时有 reason 属性,对应两种状态的返回值。

下面是返回值用法的例子。

const promises = [fetch("index.html"), fetch("https://does-not-exist/")];
const results = await Promise.allSettled(promises);

// 过滤出成功的请求
const successfulPromises = results.filter((p) => p.status === "fulfilled");

// 过滤出失败的请求,并输出原因
const errors = results
  .filter((p) => p.status === "rejected")
  .map((p) => p.reason);

有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用。

# 8.Promise.any()

该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成 fulfilled 状态,包装实例就会变成 fulfilled 状态;如果所有参数实例都变成 rejected 状态,包装实例就会变成 rejected 状态。

从判定条件上它和数组的的方法 some 很相似

Promise.any()跟 Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成 rejected 状态而结束。

const promises = [
  fetch("/endpoint-a").then(() => "a"),
  fetch("/endpoint-b").then(() => "b"),
  fetch("/endpoint-c").then(() => "c"),
];
try {
  const first = await Promise.any(promises);
  console.log(first);
} catch (error) {
  console.log(error);
}

上面代码中,Promise.any()方法的参数数组包含三个 Promise 操作。其中只要有一个变成 fulfilled,Promise.any()返回的 Promise 对象就变成 fulfilled。如果所有三个操作都变成 rejected,那么 await 命令就会抛出错误。

# 9.Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

Promise.resolve()等价于下面的写法。

Promise.resolve("foo");
// 等价于
new Promise((resolve) => resolve("foo"));

分析代码:

  1. 生成一个 Promise 对象的实例 p
  2. 状态改变为 resolved
  3. 立即执行 thenable 对象的 then()方法

# 9.1 Promise.resolve()的参数

  1. 如果参数是 Promise 实例,那么 Promise.resolve 将不做任何修改、原封不动地返回这个实例。
  2. thenable 对象指的是具有 then 方法的对象,比如下面这个对象。
  3. 如果参数是一个原始值,或者是一个不具有 then()方法的对象,则 Promise.resolve()方法返回一个新的 Promise 对象,状态为 resolved。
  4. 没有任何参数,直接返回一个 resolved 状态的 Promise 对象。
// 第二点
let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  },
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value); // 42
});
// 上面代码中,thenable 对象的 then()方法执行后,对象 p1 的状态就变为 resolved,从而立即执行最后那个 then()方法指定的回调函数,输出 42。

// 第三点
const p = Promise.resolve("Hello");

p.then(function(s) {
  console.log(s);
});
// Hello
// 上面代码生成一个新的 Promise 对象的实例 p。
// 由于字符串 Hello 不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是 resolved,所以回调函数会立即执行。
// Promise.resolve()方法的参数,会同时传给回调函数。

# 10.Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为 rejected。

const p = Promise.reject("出错了");
// 等同于
const p = new Promise((resolve, reject) => reject("出错了"));

p.catch(function(s) {
  console.log(s);
});

分析代码:

  1. 生成一个 Promise 对象的实例 p
  2. 状态改变为 rejected
  3. 立即执行回调函数

Promise.reject()方法的参数,会原封不动地作为 reject 的理由,变成后续方法的参数。

Promise.reject("出错了").catch((e) => {
  console.log(e === "出错了");
});
// true

上面代码中,Promise.reject()方法的参数是一个字符串,后面 catch()方法的参数 e 就是这个字符串。

# 总结

  • reject

    • reject方法的作用,等同于抛出错误
    • 总是用catch方法来代替then方法里的reject回调函数
  • finally

    • 不管 promise 最后的状态,在执行完 then 或 catch 指定的回调函数以后,都会执行 finally 方法指定的回调函数。
    • finally可以看做是thencatch的集合
  • all

    • Promise.all 会返回新的 promise 实例

    小结

  1. Promise 实例状态改变时会执行 then 方法内的回调函数;
  2. then 方法允许接受两个状态(成功、失败)改变的回调函数,并且返回的是新的 Promise 实例;
  3. catch 是 then 的一部分,且我们推荐使用 catch 这种直观的方式;
Last Updated: 2/15/2023, 7:45:22 PM