在上一篇深入Promise-1(juejin.cn/post/732822...)中,讲解了Promise的基础概念和使用方法,这一篇让我们接着了解Promise的一些进阶用法吧。
Promise的其他方法
ES6为Promise提供了很多内置的方法,为Promise的操作提供了更多的操作空间。
我们先整一个用来示例的异步方法:
javascript
// 模拟异步
function sync(log, time = 500, res = true) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (res) {
resolve(log);
console.log(log);
} else {
reject(log);
console.log('error', log);
}
}, time);
})
}
Promise.all
all
方法可以接收一个由多个promise实例组成的参数,并返回一个新的promise实例,all
会并发执行这些promise实例,获取所有的返回值,返回给新的promise实例。
参数
all
的参数是由多个promise实例组成。一般都是用一个数组包裹,但是在all
方法的规范中,参数其实是一个Iterator
(Promise.all(Iterator)
),一个可迭代的对象。所以除了传入数组,也可以传入迭代器对象。
javascript
// 传入数组
const p1 = Promise.all([
sync(1),
sync(2),
sync(3)
]);
// 传入迭代器对象
const myIterable = {};
myIterable[Symbol.iterator] = function () {
return {
i: 1,
next: function() {
if (this.i < 4) {
return {done: false, value: sync(this.i++)}; // 必须这么写,这是iterator返回值的规范
}
return {done: true};
}
}
};
const p2 = Promise.all(myIterable);
// 传入生成器函数
function* generate() {
yield sync(1);
yield sync(2);
yield sync(3);
}
const p3 = Promise.all(generate());
当然,除非是特殊情况,还是建议传入数组。
需要注意的是传入的Iterator
的元素必须是Promise实例,而像Map
这样的,虽然也是可迭代,但是每次迭代返回的并非Promise实例是不生效的。
javascript
// 传入Map
const pMap = Promise.all(new Map([
[1, sync(1)],
[2, sync(2)],
[3, sync(3)],
]));
pMap.then(res => {
console.log('pMap res', res);
});
console.log('pMap', pMap);
结果为:
可见Map
被当做非Promise来处理。
如果传入的参数中,含有非promise的实例,会将其使用Promise.resolve
包裹(关于Promise.resolve可以查看上篇文章)。
ini
// 传入非promise
Promise.all([
sync(1),
{ isSync: false},
sync(2)
]).then(res => {
console.log(res[1]); // {isSync: false}
});
那如果是传入一个函数呢?
javascript
// 传入非promise函数
Promise.all([
(function(){return 123})()
]).then(res => {
console.log(res[0]); // 123
});
表现与Promise.resolve
一样,但是如果在函数中抛出错误的话,也是一样,程序执行会被中断:
javascript
Promise.all(
[
(function(){
throw new Error('失败'); // Uncaught Error: 失败
})()
]
).catch(res => {
console.log('res', res);
});
假如我们传入空的 Iterator参数,all
也会将其使用Promise.resolve
包裹,并立即返回。
javascript
Promise.race([])
.then(res => {
console.log(res); // []
});
const myIterable = {};
myIterable[Symbol.iterator] = function () {
return {
next: function() {
return {done: true};
}
}
};
Promise.all(myIterable)
.then(res => {
console.log(res); // [] 打印出来是[],但其实是一个可迭代对象
});
function* generate() {}
Promise.all(generate())
.then(res => {
console.log(res); // []
});
状态
Promise.all
会返回一个新的promise,该promise的状态与传入的多个promise的状态有关。all
是并发执行传入的promise,当所有promise的状态都发生变化后,其状态才会从pending
变更。如果有一个promise是失败,则状态为rejected
,如果都是成功,则为fulfilled
。
这里有一个疑问,正常情况下(全部promise成功的情况),所有promise都会被执行,但是如果中间有某些promise执行失败,那后续的promise还会执行吗?还未执行完的promise会继续执行吗?简单做个实验:
scss
Promise.all([
sync(1, 1000),
sync(2, 1000),
sync(3, 500, false), // 第三个抛出错误
sync(4, 600)
]);
在第三个promise抛出错误,返回的promise的状态是rejected
,第一个和第二个promise并没有被停止,第四个promise也被正常执行。
结论:传入的promise并行执行,即使中间的有promise执行失败,其他promise也会正常执行,但是返回的promise的状态还是会被置为失败。失败的错误也是通过catch捕获。
返回结果
在全部promise执行成功后,Promise.all
会将所有promise的结果包装成数组返回,如果没有返回的结果的会使用undefined
占位。结果的顺序与参数中的顺序一致。如果执行失败,会返回第一个失败的原因。
scss
// 成功
Promise.all([
sync([1]),
sync({log:2}),
sync(3)
]).then(res => {
console.log(res); // [[1],{log:2},3]
});
// 失败,执到catch
Promise.all([
sync([1]),
sync({log:2}, 500, false),
sync(3)
]).catch(err => {
console.log(err); // {log:2}
});
// 多个失败,执到catch,返回第一个错误
Promise.all([
sync([1]),
sync({log:2}, 500, false),
sync({log:3}, 600, false)
]).catch(err => {
console.log(err); // {log:2}
});
使用建议
all
是并行执行,不是按顺序执行,如果执行的异步操作没有前后依赖的话,可以考虑使用all
来做并行执行。基于all
的特点,可以将需要一同获取并且不容纳失败的异步操作(例如:用户登录后获取用户基础信息和权限)包装到一起执行。假如,某些promise是允许失败的(例如那些对主流程无影响的),可以在promise上把错误catch掉,避免影响。
scss
Promise.all([
sync(1, 1000),
sync(2, 1000),
sync(3, 500, false).catch(err => {console.log(err);}), // 将错误拦截掉
sync(4, 600)
]);
Promise.race
race
方法同样可以接收一个由多个promise实例组成的参数,并返回一个新的promise实例。
scss
const p = Promise.race([
sync(1),
sync(2),
sync(3)
]);
与all
不同的是,race
虽然也是并行运行,但当一个promise实例率先改变状态,返回的promise实例的状态会随之改变,并将返回其返回值。
race
的参数 与all
也是一样,规则也一样,这里就不赘述了。有一点不同的是,如果传入一个空数组[]
,all
会将空的Iterator
作为promise的返回值,但是race
不会,返回的promise会处于pending
状态。
javascript
const p2 = Promise.race([]);
console.log(p2); // Promise { <pending> }
const myIterable = {};
myIterable[Symbol.iterator] = function () {
return {
next: function() {
return {done: true};
}
}
};
const p3 = Promise.race(myIterable);
console.log(p3); // Promise { <pending> }
function* generate() {}
const p4 = Promise.race(generate());
console.log(p4); // Promise { <pending> }
状态&返回
前面说过,race
的状态会跟随第一个改变状态的promise:
ini
const p = Promise.race([
sync(1, 600),
sync(2),
sync(3, 700)
]);
p.then(res => {
console.log('res', res);
});
可以看到,第二个promise是最先完成的,p的状态也被置为fulfilled
,返回值也置为2。而且与all
一样,并不会停止其他promise,其他promise还是会执行完成。
如果有一个promise报错呢?
ini
const p = Promise.race([
sync(1, 600, false),
sync(2),
sync(3, 700)
]);
p.then(res => {
console.log('res', res);
});
还是获取到第二个promise的结果。
如果第一个改变的状态的promise是失败的呢?
ini
const p = Promise.race([
sync(1, 600, false),
sync(2, 500, false),
sync(3, 700)
]);
p.catch(err => {
console.log('err', err);
});
抛出一个失败的错误。其他promise继续执行。
如果所有的promise都是同时返回的呢?
ini
const p = Promise.race([
sync(1),
sync(2),
sync(3)
]);
p.then(res => {
console.log('res', res);
});
按照执行机制(后面的文章聊),如果promise都是同时完成的话,理论上应该都是获取到第一个。
使用建议
利用race
的特点,我们可以实现一些需要优先处理或者竞速的场景:
- 优先渲染/加载,在并发获取数据或资源的场景,通过
race
可以判断第一个异步完成的时间点,这时候就可以做针对性的优先处理,然后其他的请求还会继续进行,可以继续完成后续的操作。 - 超时处理,在一些对时间比较敏感的场景,可以这样来实现
javascript
function timeout(time) {
return new Promise((resolve, reject) => {
setTimeout(() => reject('请求超时'), time);
});
}
Promise.race(
[
ajax('xxxx'),
timeout(500)
]
).then(() => {
// 接口未超时
}).catch(() => {
// 接口超时
})
上面的代码定义了一个timeout的函数,在到达一定的时间后会返回错误,通过race
可以在超时时做出处理。
Promise.any
any
方法也是接收多个promise实例,如果有一个promise率先成功,那么返回的promise的状态则跟随该promise的状态。这里跟race
是不一样的,如果率先完成的promise是失败的,any
会继续等待下一个成功的promise。也就是说,any
会获取第一个成功的promise,并返回对应的值。但是如果没有一个成功的promise,那就返回失败。
ini
Promise.any([
sync(1, 500, false),
sync(2, 700),
sync(3, 600)
]).then(res => {
console.log(res); // 3
}).catch(err => {
console.log(err);
});
上面的代码,第一个promise返回错误,第二第三个返回成功,但是因为第三个promise率先返回状态,所以结果返回3。
状态&返回
any
很好理解,就是返回第一个成功的promise的内容,值得注意的是,如果传入的promise全部错误或者传入空的Iterator
,会返回一个特定的错误,而不是某个promise的错误。
AggregateError
any
返回的特定错误是一个AggregateError类型的错误,
javascript
// 传入空
Promise.any([])
.catch(err => {
console.log(err); // AggregateError: All promises were rejected
console.log(err.errors); // []
});
// 全部错误
Promise.any([
sync(1, 500, false),
sync({error: true}, 700, false),
sync({message: 'error'}, 600, false)
]).catch(err => {
console.log(err); // AggregateError: All promises were rejected
console.log(err.errors); // [1, { error: true }, { message: 'error' } ]
});
从上面的例子可以看到,在全部失败的情况下,any
会等待全部promise执传完再返回,并将所有错误都收集到一个AggregateError 对象中。直接访问这个错误对象,会告知All promises were rejected
,这是一个固定的错误信息,同时可以通过AggregateError.errors来获取所有promise返回的错误。
关于AggregateError,可以参考MDN的说明:developer.mozilla.org/zh-CN/docs/...
使用建议
any
比较适合一些竞速和容错的场景:
- 容错: 为了提升用户体验,使用
any
可以在多个服务器获取同一数据或资源,可以避免单个请求失败后无法获取数据或资源的情况。这不仅提高了数据获取的效率,也增加了系统的容错能力,使得用户在使用应用时获得更加流畅和稳定的体验。此外,这种方法也有助于分散对单一服务器的依赖,减少单点故障的风险。例如实现一个在线播放器,可以从多个资源服务器获取同一个音频,即可保证最快的加载,又可以避免单个资源服务器失败后复杂的重试机制。 - 竞速: 跟
race
的类似的方式,但是any
的好处就是具备容错性,除了全部失败,总能获得结果。
Promise.allSettled
allSettled
同样是接收Iterator
参数,该方法会在所有的promise状态都发生变化后(无论成功和失败)返回一个成功的promise实例,并将所有的promise实例的结果包装成一个数组返回。
ini
const p1 = Promise.allSettled([
sync(1),
sync(2),
sync(3, 500, false)
]);
p1.then(res => {
console.log(res);
});
上面的代码得到的res为:
lua
[
{ status: 'fulfilled', value: 1 },
{ status: 'fulfilled', value: 2 },
{ status: 'rejected', reason: 3 }
]
p1则为fulfilled
的promise实例。
状态
allSettled
返回的promise实例的状态只会是fulfilled
,无论参数是空的Iterator
还是全部失败的promise实例,都是fulfilled
。
scss
Promise.allSettled([
sync(1, 500, false),
sync(2, 500, false),
sync(3, 500, false)
]); // fulfilled
Promise.allSettled([]); // fulfilled
返回
allSettled
回调的返回值则是固定的格式,会将所有promise实例的返回值包装进一个数组,数组的元素为固定格式的对象:
- 成功:
{ status: 'fulfilled', value: value }
- 失败:
{ status: 'rejected', reason: reason }
对象的属性意义如下:
- status 对应的promise的状态,成功为
fulfilled
,失败为rejected
- value 成功的promise会返回该属性,值为promise的返回值
- reason 失败的promise会返回该属性,只为promise失败的原因
返回复杂结构的值或者错误对象,结果也是一样的:
less
Promise.allSettled([
sync({result:'success', data: [{id:1, value:1}]}),
sync(new Error('失败'), 500, false)
]).then(res => {
console.log(res);
});
使用建议
allSettled
比较适合需要容错的情况:
- 多并发容错 ,在一个需要多异步并发的场景,通过
allSettled
可以在then
中统一处理 - 初始化时的资源加载 ,在程序初始化时可能需要加载多个多种资源(图片、音频、配置文件等),这些资源即使加载失败也不应该影响程序的继续执行,可以使用
allSettled
来处理。 - 回退或者重试 ,例如在node中做些数据库的写入操作,在批量写入或者是事务性操作中,使用
allSettled
可以获得所有异步读写操作的结果,通过对结果判断是否需要回退数据或者重试读写(使用all
无法实现这种效果,如果出现错误只能得到一个错误)。
其他用法
Promise进阶用法,最基础就是链式调用,另外就是前面聊得内置方法的使用。哪还有其他使用方法吗?
Deferred式外部控制
Deferred是早期Promise还未纳入ES规范中,第三方库实现Promise提供的一种使用方法,可以在Promise外部控制Promise的状态:
ini
function defered() {
const dfd = {
promsie: null,
resolve: null,
reject: null
};
dfd.promise = new Promise(function(resolve, reject) {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
function demo() {
const dfd = defered();
// dosomething
if (success) {
dfd.resolve();
} else {
dfd.reject();
}
return dfd.promise;
}
上面的代码应该是最简单的用法defered
函数返回了一个包含了空的Promise实例(没有包含任何异步操作)、resolve方法、reject方法的对象,在demo
函数中返回Promise实例,在某些的操作后执行再改变其状态。这种写法可以将demo
内部的异步操作与返回Promise隔离开来,这样通过demo
得到的就是一个属于demo
的promise,调用方不会感知到demo
内部的异步操作也不会受其影响。另外就是可以根据需要在未来某个时间点去修改返回的promise的状态,已达到根据需要修改状态的需求。
在Promise纳入ES规范后,上面的方法可以直接使用Promise.resovle()
和Promise.reject()
来实现,比Deferred更简洁理解难度更低。
当然,Deferred还是可用的地方,例如在页面上的一些回调,无法只直接传递Promise,可以用Deferred来实现。例如在Vue中实现一个全局确认弹窗:
弹窗有两个按钮:确认和取消,希望实现这样的效果,通过一个全局的方法来唤醒弹窗,方法返回一个Promise,在点击确定后通过then回调,取消通过catch回调。这样就可以直接在js中调用又不需要写一堆监听方法。\
dialog.vue代码
xml
<template>
<div
class="dialog"
v-show="visible"
>
<div>
{{ message }}
</div>
<button @click="cancel">取消</button>
<button @click="submit">确认</button>
</div>
</template>
<script setup>
defineProps({
visible: Boolean,
message: String,
cancel: Function,
submit: Function
})
</script>
入口文件index.js代码
ini
import { createApp } from 'vue'
import MessageDialog from './dialog.vue';
function defered() {
const dfd = {
promsie: null,
resolve: null,
reject: null
};
dfd.promise = new Promise(function(resolve, reject) {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd;
}
export default (message) => {
const dfd = defered();
const mountNode = document.createElement('div');
const Instance = createApp(MessageDialog, {
visible: true,
message,
cancel: () => {
dfd.reject();
},
submit: () => {
dfd.resolve();
}
});
document.body.appendChild(mountNode);
Instance.mount(mountNode);
return dfd.promise;
}
使用
javascript
import messageDialog from '@/components/message-dialog';
messageDialog('测试dialog')
.then(() => {
console.log('submit')
})
.catch(() => {
console.log('cancel')
})
OK,上面的代码只是一个简单的示例,展示下这种外部修改Promise状态的用法,提供一些参考。
异步结果共享
在项目运行中,可能会出现多个调用方调用同一个异步操作获取相同的结果的情况,例如token过期后,同一时间有多个曹邹调用刷新token的接口。这种情况会造成大量的资源浪费,我们可以利用Promise状态确认后不会改变的特性来实现一定时间内的异步结果共享。
ini
function ajaxShare(url = '', data = {}, type = 'GET', duration = 0)
{
const key = JSON.stringify(url, data, type);
const curPromise = promises[key];
if (curPromise) {
if (curPromise.expiration > +new Date()) {
return curPromise.promise;
}
delete promises[key];
}
promises[key] = {
promise: ajax(url, data, type),
expiration: +new Date() + duration
};
return promises[key].promise;
}
上面的代码对上篇的ajax
方法做了简单的封装,对于同一个请求,可以在指定的时间内都是返回同一个promise,实现同一请求的结果共享:
ini
const p1 = ajaxShare('url', {}, 'GET', 500);
const p2 = ajaxShare('url', {}, 'GET', 500);
console.log(p1 === p2); // true
总结
本文总结了一些Promise的进阶方法,结合个人的经验给了一些使用的建议。希望对大家有帮助。接下来会研究下Promise的原理,敬请期待。