前言:
定期复盘---今天我们来复习一下 Promise 的几个方法,分别是:Promise.resolve、Promise.reject、Promise.then、Promise.catch、Promise.finally、Promise.all、Promise.allSettled、Promise.race、Promise.any;
接下来我们一起去看看吧,从深处去了解他们有什么区别!!!
目录
Promise 状态
1. Promise.resolve
2. Promise.reject
3. Promise.then
① 函调函数异步执行
② 返回值
③ promise穿透
4. Promise.catch
① 语法糖的本质
② 只有一个主人
5. Promise.finally
6. Promise.all
7. Promise.allSettled
8. Promise.race
9. Promise.any
Promise 状态
- 初始状态 -> pending
- 初始状态可以改变
- 在resolve 或者 reject 调用之前都处于这个状态
- 最终成功状态 -> fulfilled
- 执行 resolve 函数,状态改变为 fulfilled
- 执行 onFulfilled 函数
- 最终失败状态 -> rejected
- 执行 reject 函数,状态改变为 rejected
- 执行 onRejected 函数
- then 方法
- then 方法为 Promise 对象注册了 onFulfilled 和 onRejected 函数
- catch 方法
- catch 方法为Promise 对象注册了 onRejected 函数
1. Promise.resolve
静态方法 Promise.resolve(value)
可以认为是 new Promise
方法的语法糖,比如Promise.resolve(42)
可以认为是以下代码的语法糖。
new Promise(function (resolve) {
resolve(42)
})
这个静态方法会让Promise
对象立即进入确定(即resolved) 状态,并将42传递给后面 then 里所指定的 onFulfilled
函数。作为 new Promise
的快捷方式,在进行 Promise
对象的初始化或者编写测试代码的时候都非常方便。
简单总结一下 Promise.resolve
方法的话,它的作用就是将传递给它的参数填充 Fulfilled
到 Promise
对象后并返回这个 Promise
对象。
2. Promise.reject
Promise.reject(error)
是和Promise.resolve(value)
类似的静态方法,是 new Promise
方法的快捷方式。比如 Promise.reject(new Error("Promise reject error"))
就是下面代码的语法糖形式
new Promise(function (reject) {
reject(new Error("Promise reject error"))
})
简单总结一下 Promise.reject
方法的话:它的功能是调用该 Promise
对象通过then指定的 onRejected
函数,并讲错误(Error)对象传递给这个onRejected
函数
3. Promise.then
Promise.then(onFulfilled, onRejected)
① 函调函数异步执行
var promise = new Promise((resolve, reject) => {
console.log("inner Promise"); // 1
setTimeout(() => {
resolve("Fashion Barry"); // 3
}, 1000);
});
promise.then((res) => {
console.log("res", res);
});
console.log("outer promise"); // 2
// Promise 实际是一个同步函数,then 方法才是异步
// 所以输出顺序如上
Promise/A+规范
统一规定:Promise 只能使用异步调用方式
② 返回值
不管你在回调函数 onFulfilled
中会返回一个什么样的值,或者不返回值,该值都会由 Promis.resolve(return 的返回值)
进行响应的包装处理。因此,最终 then
的结果都是返回一个新创建的 Promise
对象。
也就是说,Promis.then
不仅仅是注册一个回调函数那么简单,它还会将回调函数的返回值进行变换,创建并返回一个Promise
对象。正是 then
函数中有了这样返回值的机制,才能使得在整个Promise
链式结构当中,每个then
方法都能给 下一个then
方法传递参数。现在我们知道怎么返回的Promise
是之前的还是新的?另外该Promise
的状态又是如何?
var aPromise = new Promise((resolve, reject) => {
resolve("aPromise");
});
var thenPromise = aPromise.then((res) => {
console.log("thenPromise: ", res);
});
var catchPromise = aPromise.catch((err) => {
console.error("catchPromise: ", err);
});
console.log(aPromise !== thenPromise); // true
console.log(thenPromise !== catchPromise); // true
console.log(aPromise, thenPromise, catchPromise); // Promise { "aPromise" }, Promise { <pending> }, Promise { <pending> }
从上面结果来看,实际上不管是调用 then
还是catch
方法, 都返回了一个新的Promise
对象
③ promise穿透
我们先来举个例子:
Promise.resolve("Barry")
.then(Promise.resolve("Barry Promise"))
.then((result) => {
console.log("result", result); // "Barry"
});
如果你认为输出的是【 Barry Promise 】,那么你就错了,实际上他输出的是 【 Barry 】
产生这么的输出是因为你给then
方法传递了一个非函数(比如promise
对象)的值,代码会这样理解 : then(null)
,因此导致了前一个promise
的结果产生了坠落的效果,也就是和下面代码是一样的, 代码直接穿透了then(null)
进入了下一层链:
Promise.resolve("Barry")
.then(null)
.then((result) => {
console.log("result", result); // "Barry"
});
随意添加多个then(null)
结果都是一样的
Promise.resolve("Barry")
.then(null)
.then({ name: "My name is Barry" })
.then(null)
.then((result) => {
console.log("result", result); // "Barry"
});
4. Promise.catch
① 语法糖的本质
这里我们再说一遍,实际上Promise.catch
只是promise.then(undefined, onRejected)
方法的一个别名而已。也就是说,这个方法用来注册当Promise
对象状态变为 Rejected
时 的回调函数。可以看下面代码,两者写法是等价的,但是很明显 Promise.catch
会让人第一眼看上去不会眼花缭乱:
// 第一种写法
Promise.resolve()
.then((data) => console.log(data))
.then(undefined, (err) => console.log(err));
// 第二种写法
Promise.resolve()
.then((data) => console.log(data))
.catch((err) => console.log(err));
那么我们现在来说说为什么推荐使用第二种方法,而不是第一种:
- 使用
promise.then(onFulfilled, onRejected)
的话,在onFulfilled
中发生异常的话,onRejected
中是捕获不到这个异常的。而且如果链式很长,每一条链上都要这么写。 - 在
promise.then(onFulfilled).catch(onRejected)
的情况下.then
中产生异常能在.catch
中捕获。.then
和.catch
本质上是没有区别的, 需要分场合使用
② 只有一个主人
我们上面已经说过了,在书写很长的Promise
链式,从代码清晰度和简易程度来讲,在最后添加 catch
是远远在每一层链上写onRejected
回调函数是要好的,因为catch
可以捕获 Promise
链中每一层节点的错误,这句话本身没有错,但从这句话延伸出一种错误的理解:catch
同时监控着所有节点。实际上catch
函数在同一个时间点只属于某一个Promise
,因为它的主人是随着程序 的执行而不断变化的,我们来举个例子:
let p1 = new Promise((resolve, reject) => {
// 第一层执行逻辑
resolve("first promise"); // Promise(1)
})
.then((res) => {
// 第二层执行逻辑
return "second promise"; // Promise(2)
})
.then((res) => {
// 第三层执行逻辑
return "third promise"; // Promise(3)
})
.catch((err) => {
console.log("err", err);
});
在上述例子中,如果整个程序每一步都正确执行,那么会顺序产生三个Promise
对象,分别是 Promise(1)
,Promise(2)
,Promise(3)
:
- 可是如果在第一层具体执行逻辑出错了后,那实际上后面的两个
then
中的回调函数压根不会被异步执行,所以会直接异步触发catch
中的回调函数执行, 所以这种情况下catch
是Promise(1)
对象的catch
。 - 如果第一层具体执行逻辑正确执行,就会异步触发第二个
then
中的回调函数执行,那么同理 ,在第二次具体执行逻辑抛出错误,会导致Promise(2)
的状态变化,所以这种情况下catch
是Promise(2)
对象的catch
。 - 同理
Promise(3)
也是如此
总结下来就是:整个Promise
链中,catch
只属于异步触发它当中回调函数 执行的那个Promise
,并不属于所有 Promise
5. Promise.finally
promise.finally
方法的回调函数不接受任何参数,这意味着finally
没有办法 知道,前面的Promise
状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与Promise
状态无关的,不依赖于 Promise
的执行结果。我们来看下面代码:
var p1 = new Promise((resolve, rejevt) => {
setTimeout(() => {
resolve;
}, 1000);
});
p1.then((res) => console.log(res))
.catch((err) => console.log(err))
.finally(() => console.log("finally"));
finally
本质上是then
方法的特例。我们来看下面伪代码:
promise.finally(() => {
// 执行逻辑
});
// 上面代码等同于下面
promise.then(
(onFulilled) => {
// 语句
return onFulilled;
},
(onRejected) => {
// 语句
throw onRejected;
}
);
上面代码中,如果不使用finally
方法,同样的语句需要为成功和失败的状态各写一次。 有了finally
方法,则只需要写一次。那么它是如何实现的呢?
Promise.prototype.finally = function (callback) {
let p = this.constructor;
return this.then(
(value) => p.resolve(callback()).then(() => value),
(reason) =>
p.reject(callback()).then(() => {
throw reason;
})
);
};
var p = new Promise((resoleve, reject) => {
setTimeout(() => {
reject("Promise err");
}, 1000);
});
p.catch((err) => console.log("err", err)).finally(() => {
console.log("finally");
});
上述代码中,不管前面的Promise
是fulfilled
还是rejected
,都会执行回调函数callback
6. Promise.all
Promise.all
接受一个promise
对象的数组作为参数,当这个数组里的所有 Promise
对象 全部变为resolve
或者reject
状态的时候,它才会去调用.then
方法。
传递给Promise.all
的 promise
并不是一个个的顺序执行的,而是同时开始、并行执行的,我们可以看下面例子
var promise1 = new Promise((resoleve, reject) => {
setTimeout(() => {
resoleve("promise1--3000");
}, 3000);
});
var promise2 = new Promise((resoleve, reject) => {
setTimeout(() => {
resoleve("promise2--1000");
}, 1000);
});
var promise3 = new Promise((resoleve, reject) => {
setTimeout(() => {
resoleve("promise3--5000");
}, 5000);
});
var promiseArr = [promise1, promise2, promise3];
console.time("promiseArr");
Promise.all(promiseArr)
.then((res) => {
console.log("res", res); // ['promise1--3000', 'promise1--1000', 'promise1--5000']
console.timeEnd("promiseArr"); // 5523.29296875 ms
})
.catch((err) => console.log(err));
为什么这个例子可以看出来Promise.all()
是并行的呢?因为所有Promise
执行完只用了5秒,如果3个 Promise
是按照顺序执行的,那么应该是9秒或者,在5-9之间,因为4个Promise
并不是同时执行的,同时执行的 话总时间就是那个花费时间最长的Promise
Promise.all()
重要细节点 (面试常考):
- 如果所有的
Promise
中只有一个执行错误,那么整个Promise.all
不会走Promise.all().then()
而是走Promise.all().catch()
这个流程了。但是要注意的是虽然走到了Promise.all().catch()
这个流程 ,但是其他Promise
还是会正常执行,但不会返回结果 - 要注意
Promise.all()
的返回值顺序,Promise.all().then()
的返回值顺序和传入的顺序是一致的,笔试时 遇到手写Promise.all()
时要注意。
7. Promise.allSettled
Promise.allSettled()
的入参和Promise.all、Promise.race
一样,接受一个promise
对象的数组作为参数,也是同时开始、并行执行的。但是Promise.allSettled
的返回值需要注意以下几点:
Promise.allSettled
不会走进catch,当所有输入Promise
都被履行或者拒绝时, statusesPromise
会解析一个具有具体完成状态的数组
{ status: 'fulfilled', value:value }
:如果相应的promise
被履行{ status: 'rejected', reason: reason }
:如果相应的promise
被拒绝
我们看下面示例:
var promise1 = new Promise((resoleve, reject) => {
setTimeout(() => {
reject(new Error("promise1--3000"));
// resoleve("promise1--3000");
}, 3000);
});
var promise2 = new Promise((resoleve, reject) => {
setTimeout(() => {
// reject(new Error("promise1--1000"))
resoleve("promise2--1000");
}, 1000);
});
var promise3 = new Promise((resoleve, reject) => {
setTimeout(() => {
resoleve("promise3--5000");
// reject(new Error("promise1--5000"))
}, 5000);
});
var promiseArr = [promise1, promise2, promise3];
console.time("promiseArr");
Promise.allSettled(promiseArr)
.then((res) => {
console.log("res", res);
console.timeEnd("promiseArr");
})
.catch((err) => console.error(err))
.finally(() => console.log("finally"));
总结一下:Promise.allSettled()
在你需要执行平行和独立的异步操作并收集所有结果时非常有效, 即使某些异步操作可能失败。
8. Promise.race
Promise.rece()
的使用方法和 Promise.all
一样,接收一个promise
对象的数组为参数,Promise.race
是要有一个promise对象进入Fulfilled
或者 Rejected
状态的话,就会继续进行后面的处理。这里依旧有两个点要注意:
- 和
Promise.all
一样是所有数组当中的Promise
同时并行的 Promise.race
在第一个Promise
对象变为Fulfilled
之后,并不会 取消其他promise
对象的执行。Promise.race
接受的是一个Promise
对象数组,但是返回的确实最先完成Fulfilled
或者最先被Rejected
的一个Promise的结果
下面我们来举个例子:
let arr = [1000, 3000, 5000, 7000];
let promiseArr = [];
for (let i = 0; i < arr.length; i++) {
let newPromise = new Promise((resolve, reject) => {
if (i === 0) {
reject(new Error("第二个错误"));
} else {
setTimeout(() => {
console.log(arr[i]);
resolve(arr[i]);
}, arr[i]);
}
});
promiseArr.push(newPromise);
}
Promise.race(promiseArr)
.then((res) => {
console.log(res);
})
.catch((err) => {
console.log(err);
});
// 控制台报错
// 3000
// 5000
// 7000
这里我们再复习一下Node
当中事件循环的知识:
- 第一层循环:i为0时,异步触发了
Promise.race().catch()
,这里面的回调代码被放在了微任务队列中, 后面的3个setTimeout
宏任务的回调函数代码被放进了timer
阶段中的队列当中(其实并不是这样,因为 三个定时器都有延迟,都是在后面的事件循环中添加进来的) - 第二层循环:清空微任务对列,所以控制台打印出了错误,然后清空宏任务,分别打印出
3000/5000/7000
9. Promise.any
Promise.any
的入参和Promise.all、Promise.race、Promise.allSettled
一样, 接收一个promise
对象的数组作为参数。
- 只要其中有一个
Promise
成功执行,就会返回已经成功执行的Promise
的结果 - 如果这个
promise
对象的数组中没有一个promise
可以成功执行(即所有的promise
都失败 ),就返回一个失败的promise
和AggregateError
类型的实例,它是Error
的一个子类,用于把单一的错误集合 在一起
var promise1 = new Promise((resoleve, reject) => {
setTimeout(() => {
// reject(new Error("promise1--3000"));
resoleve("promise1--3000");
}, 3000);
});
var promise2 = new Promise((resoleve, reject) => {
setTimeout(() => {
// reject(new Error("promise2--1000"))
resoleve("promise1--1000");
}, 1000);
});
var promise3 = new Promise((resoleve, reject) => {
setTimeout(() => {
// resoleve("promise3--5000");
reject(new Error("promise1--5000"))
}, 5000);
});
var promiseArr = [promise1, promise2, promise3];
console.time("promiseArr");
Promise.any(promiseArr)
.then((res) => {
console.log("res", res); // res promise1--1000
console.timeEnd("promiseArr");
})
.catch((err) => console.error(err));
//所有的Promise都失败, AggregateError: All promises were rejected
总计一下Promisea.any
的应用场景:如果我们现在有多台服务器,则尽量使用响应速度最快的服务器,在这种情况下, 可以使用Promise.any()
方法从最快的服务器接收响应。