前言
最近看很多掘友都在说现在面试特喜欢考手写Promise.all、Promise.any等方法,我突然想起来也是最近才注意到的一个Promise.all的细节,相信相当多的人会把这个细节忽略掉,我搜了一下相关文章,果然不出所料,很多人掉坑了,所以我觉得有必要写一篇文章来好好唠唠这个事。
Promise.all
先看MDN上的定义:
Promise.all() 方法接收一个 promise 的 iterable 类型(注:Array,Map,Set都属于ES6的iterable类型)的输入,并且只返回一个
Promise
实例, 那个输入的所有 promise 的 resolve 回调的结果是一个数组。这个
Promise
的 resolve 回调执行是在所有输入的 promise 的 resolve 回调都结束,或者输入的 iterable 里没有 promise 了的时候。它的 reject 回调执行是,只要任何一个输入的 promise 的 reject 回调执行或者输入不合法的 promise 就会立即抛出错误,并且reject的是第一个抛出的错误信息。
我们来一句一句分析
Promise.all() 方法接收一个 promise 的 iterable 类型(注:Array,Map,Set都属于ES6的iterable类型)的输入
Promise.all接受的参数是一个可迭代的对象,换句话说就是具备迭代器(iterator)接口的数据结构,大家如果不了解可迭代对象的话,推荐查看这篇文章彻底搞懂,别忽视迭代器(Iterator),你每天都在与它打交道。
我们先用原生的Promise.all看一下,传入非可迭代类型会怎么样:
可以看到,传入非可迭代类型会报reject,我们先来写这部分
// 给Promise添加静态方法myAll,因为原生的all就是静态方法
Promise.myAll = function (promises) {
if (! typeof promises[Symbol.iterator] === 'function') { // 检查是否是可迭代类型
const type = typeof promises;
// 拼接错误提示字符串,基本数据类型要把值也提示给开发者
return Promise.reject(`${type} ${type === 'object' ? '' : promises} is not iterable (cannot read property Symbol(Symbol.iterator))`);
}
};
看下效果
nice! 符合预期,接下来分析下一句
并且只返回一个
Promise
实例, 那个输入的所有 promise 的 resolve 回调的结果是一个数组
提炼: 返回的是Promise实例,返回值是数组
// 给Promise添加静态方法myAll,因为原生的all就是静态方法
Promise.myAll = function (promises) {
if (! typeof promises[Symbol.iterator] === 'function') { // 检查是否是可迭代类型
const type = typeof promises;
return Promise.reject(`${type} ${type === 'object' ? '' : promises} is not iterable (cannot read property Symbol(Symbol.iterator))`); // 基本数据类型要把值也提示给开发者
}
const results = []; // 执行结果数组
return new Promise((resolve) => { // 返回Promise
resolve(results);
});
};
接着再看
这个
Promise
的 resolve 回调执行是在所有输入的 promise 的 resolve 回调都结束,或者输入的 iterable 里没有 promise 了的时候。
简单说就是要等所有promise都执行成功,可迭代对象里不是promise的数据转成promise后直接resolve,全部执行成功后才能执行Promise.all的resolve回调。
看下原生传入不是promise的数据是啥效果
不是promise的数据会立即resolve并返回原值,所以我们不能把所有传过来的都当做Promise对象而直接调用.then(),用Promise.resolve()
包裹一下之后就可以了,如:
// 这样就可以调用then方法了
Promise.resolve('666').then();
Promise.resolve({a: 3}).then();
我们把上面分析的实现一下:
// 给Promise添加静态方法myAll,因为原生的all就是静态方法
Promise.myAll = function (promises) {
if (! typeof promises[Symbol.iterator] === 'function') { // 检查是否是可迭代类型
const type = typeof promises;
return Promise.reject(`${type} ${type === 'object' ? '' : promises} is not iterable (cannot read property Symbol(Symbol.iterator))`); // 基本数据类型要把值也提示给开发者
}
let doneCount = 0; // 执行成功计数器
const results = []; // 执行结果数组
return new Promise((resolve) => {
// Object.entries将promises转换为[ ['0', 'a'], ['1', 'b'], ['2', 'c'] ]这种键值对数组,便于获取索引
for (const [index, item] of Object.entries(promises)) { // 获取遍历项和索引
Promise.resolve(item).then((res) => {
results[index] = res; // index的作用是保证输出结果的顺序与输入保持一致
doneCount += 1;
if (doneCount === promises.length) return resolve(results); // 全部成功时resolve
});
}
});
};
看下效果。
完美!
TIPS
:这里有的同学可能有疑问了,为啥没用forEach实现,我看网上很多手写的Promise都是用forEach实现的啊,用forEach是不是也可以啊?
严格来说,不行!我们最开始有提到,Promise.all接受的是可迭代对象为入参,即Promise.all(iterable)
,而forEach并不能遍历所有可迭代对象。
// 创建一个可迭代对象
const iterator = {
i: 0,
next() { // 实现next()接口
if (this.i > 10) return {value: undefined, done: true };
return { value: this.i++, done: false };// this.i++ 将迭代器指向下一个元素
},
// 实现对象的迭代器接口,然后它就可以被遍历了
[Symbol.iterator]() {
return this; // 因为这个对象本身就是迭代器对象,所以可以用自身作为自己的迭代器
}
}
// 上面这部分如果不理解的话建议看我的另一篇文章《别忽视ES6迭代器(Iterator),你每天都在与它打交道》,花5分钟搞懂它
// 先用forof遍历一下,输出正常
for (const item of iterator) {
console.log(item); // 0,1,2,...,10
}
// 再用forEach遍历一下,报错
iterator.forEach((item) => {
console.log(item) // Error: iterator.forEach is not a function
})
不妨用forEach写出来试一下
而原生的all方法是可以正常执行并返回数据的
所以,你用forEach实现的实际上是个半成品,forof
可以遍历所有可迭代对象,所以用for of更合适。
但是,变通一下,我们还是可以使用forEach的,但是很多同学实际上都忽略了这一点
// 如果要使用forEach可以与扩展运算符,或Array.from方法配合,将可迭代对象转换为数组,forEach可以迭代数组
Promise.myAll = function (promises) {
if (! typeof promises[Symbol.iterator] === 'function') { // 检查是否是可迭代类型
const type = typeof promises;
return Promise.reject(`${type} ${type === 'object' ? '' : promises} is not iterable (cannot read property Symbol(Symbol.iterator))`); // 基本数据类型要把值也提示给开发者
}
const arr = [];
let count = 0;
return new Promise((resolve, reject) => {
// 或者 const promsArr = [...promises];
Array.from(promises).forEach((item, i) => {
Promise.resolve(item).then((res) => {
arr[i] = res;
count += 1;
if (count === promises.length) resolve(arr);
}, reject);
});
});
ok,了解了使用forEach的坑之后我们接着分析,现在还差最后一哆嗦
它的 reject 回调执行是,只要任何一个输入的 promise 的 reject 回调执行或者输入不合法的 promise 就会立即抛出错误,并且reject的是第一个抛出的错误信息。
任意一个promise发生错误,就会抛出错误,并且reject的是第一个抛出的错误信息,先看下原生咋处理错误的
来吧,开搞
// 给Promise添加静态方法myAll,因为原生的all就是静态方法
Promise.myAll = function (promises) {
if (! typeof promises[Symbol.iterator] === 'function') { // 检查是否是可迭代类型
const type = typeof promises;
return Promise.reject(`${type} ${type === 'object' ? '' : promises} is not iterable (cannot read property Symbol(Symbol.iterator))`); // 基本数据类型要把值也提示给开发者
}
let doneCount = 0; // 执行成功计数器
const results = []; // 执行结果数组
return new Promise((resolve, reject) => {
for (const [index, item] of Object.entries(promises)) {
Promise.resolve(item).then((res) => {
results[index] = res; // index的作用是保证输出结果的顺序与输入保持一致
doneCount += 1;
if (doneCount === promises.length) return resolve(results); // 全部成功时resolve
}, reject); // 别的地方不变,在这里加上reject,错误时直接调用外层Promise的reject函数,或者你先catch再返回也可以
}
});
};
本次改动很简单,在then的第二个参数直接调用reject,看下效果:
与原生一致,完美!至此,Promise.all我们就实现完啦,顺着我的思路来,相信你已经完全掌握了,新技能get!
感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,点赞、关注、收藏,作者会持续与发家分享更多干货。