# (六)Promise 初探
TIP
这篇文章算是我对 Promise 其中基础部分的理解,因为阮一峰老师的 ES6 基础已经足够好了,本文更像是一个字典,我也会尽量串联起相关的知识点。 注意,本篇文章更像示例解释,下一篇我们一起研究点有意思的,共勉~
# 1.什么是 Promise
- 是异步编程的一种解决方案
- 是一个容器,里面保存着某个未来才会结束的事件的结果。
- 是一个对象,从它可以获取异步操作的消息。
Promise 对象有以下两个特点:
- 对象的状态不受外界影响:
- Promise 对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)
- 只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
- 一旦状态改变,就不会再变:任何时候都可以得到这个结果。
- Promise 对象的状态改变,只有两种可能:从
pending
变为fulfilled
和从pending
变为rejected
。 - 如果改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。
- Promise 对象的状态改变,只有两种可能:从
优点
- Promise 对象,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。
- 此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
缺点
- 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
- 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
# 2.基本用法
Promise构造函数
接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供。
- resolve 函数的作用是,将 Promise 对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
- 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: 报错了]
分析代码:
- p1 实例正常 resolve
- p2 首先会 rejected,
- p2 有自己的 catch 方法,捕获错误并返回了一个新的 Promise 实例,p2 指向的实际上是这个实例。
- 新实例执行完 catch 方法后,也会变成 resolve
- 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
属性,对应两种状态的返回值。
- fulfilled 时,对象有
下面是返回值用法的例子。
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"));
分析代码:
- 生成一个 Promise 对象的实例 p
- 状态改变为 resolved
- 立即执行 thenable 对象的 then()方法
# 9.1 Promise.resolve()的参数
- 如果参数是 Promise 实例,那么 Promise.resolve 将不做任何修改、原封不动地返回这个实例。
- thenable 对象指的是具有 then 方法的对象,比如下面这个对象。
- 如果参数是一个原始值,或者是一个不具有 then()方法的对象,则 Promise.resolve()方法返回一个新的 Promise 对象,状态为 resolved。
- 没有任何参数,直接返回一个 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);
});
分析代码:
- 生成一个 Promise 对象的实例 p
- 状态改变为 rejected
- 立即执行回调函数
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
可以看做是then
和catch
的集合
all
- Promise.all 会返回新的 promise 实例
小结
- Promise 实例状态改变时会执行 then 方法内的回调函数;
- then 方法允许接受两个状态(成功、失败)改变的回调函数,并且返回的是新的 Promise 实例;
- catch 是 then 的一部分,且我们推荐使用 catch 这种直观的方式;