目录
大家面试时,面试官可能会让手写一个promise.all和promise.prototype.finally方法,我在手写时候遇到了很多坑,现在记录下来,分享给大家。
Promise.all
听到让手写一个Promise.all方法,有的同学会这样写
Promise.prototype.all = function () {}
好多同学第一反应就是这么写,习惯往原型链上去挂一个方法,认为all也是原型链上的方法,很多人都不不用在意这个东西,感觉会实现内部逻辑就可以了,但其实这也是很重要的一项,通过写最开始的这一段赋值,就可以了解到你知不知道什么叫实例方法什么叫静态方法。
平常使用promise.all时,一般都是去这样调用,其实all就是挂载在Promise这个大对象上的,是一个静态方法,应该这样写。
Promise.all([]).then()
而实例方法是通过new Promise之后做到的。
new Promise().then() // 这才是实例方法
当去实现一个promise.all方法时一定记住不要把all挂载在原型链上,要去实现时,直接这样写就好。
Promise.all = () => { }
首先知道all是一个静态方法,返回值是一个promise,接受一个参数,参数是数组,整体返回值是一个promise。
Promise.all = (arr) => {
return new Promise((resolve, reject) => {})
}
promise.all特性
-
接受一个数组,里面可能是一些promise元素
-
是要等所有的promise执行完成之后,然后去resolve整体结果
-
其中一个报错就会reject
接收到了一个数组,把数组里面完全执行一遍首先想到可以用遍历,便利完之后 里面每一项就是 arr[i],但是有问题,数组里面每一项元素不一定是一个promise,有可能传入的是这么个东西。
Promise.all([1, 2, 3, new Promise()])
这个时候如果直接去 arr[i].then 可能会报错 因为它可能是个简单基本类型,这也有可能是面试官希望考察的一个点。
基于以上情况,有的同学会这样写。
Promise.all = (arr) => {
return new Promise((resolve, reject) => {
for (let i = 0; i < arr.length; i++) {
if (arr[i] instanceof Promise) {
//
} else {
//
}
}
})
}
用 instanceof 判断一下是否是promise,在if和else根据不同类型实现不同逻辑,这样当然能够实现,但是有个问病就是 if 和 else 里面的逻辑几乎是一摸一样的,只不过要做一个类型处理而已,就会导致很大一部分代码的冗余。
其实这里不推荐去手动判断arr[i]的一个数据类型,既然不知道arr[i]是个什么类型,不如直接把arr[i]强制转换为Promise,这样就比较方便了,可以把所有的逻辑都写在这个Promise的.then或者.catch里面去。
then里面会拿到一个value,catch里面会拿到一个reason,如果其中有一个报错,就要在.catch里面直接reject。
.then里面逻辑,首先判断它到底是不是把所有的元素都执行完成,有的同学可能会在for循环上面从以一个结果数组res,然后res.push(value),这样会出现结果数组里面的顺序问题,每当执行完一个arr[i]之后都会把结果value,push到res里面,造成一个结果,数组里面的顺序打乱。
promise.all的返回结果是按照入参数组的顺序来决定的,一旦使用了push方法(把一个元素一次存进数组的末尾),同步执行是没问题的,但是要知道promise.then是异步的,每一个promise的执行时间是不一定的。
比如有个数组 [1, 2 ,3 ,4 ,5],1~4的执行时间是100ms,但是5的执行时间是1ms,因为他是异步操作,5肯定是最先执行完成的,那么res数组里面的最后的结果就是[5, 1, 2, 3, 4]。
如何写才能避免这个问题呢,直接按照索引赋值就不会出现顺序问题。
Promise.all = (arr) => {
return new Promise((resolve, reject) => {
let res = [];
for (let i = 0; i < arr.length; i++) {
Promise.resolve(arr[i])
.then((value) => {
// res.push(value); 错误写法
res[i] = value; // 正确写法
})
.catch((reason) => {
reject(reason);
})
} })
}
避免上一个坑之后,有的同学接着又会这样写,res数组的长度和arr数组的长度一样时候,代表为整个promise数组都执行完成了,所有的结果都推进res里面了,直接resolve结果。
问题:和上个问题一样,5执行时间为1ms,执行完毕后,res[4] = value,这个时候res和arr数组的长度就一样了,这个时候resolve(res),就会导致1~4还没执行完成,res数组里面前四个都是空属性,就把结果返回回去了。
正确写法:定义一个变量count来计数。
Promise.all = (arr) => {
return new Promise((resolve, reject) => {
let res = [];
let count = 0;
for (let i = 0; i < arr.length; i++) {
Promise.resolve(arr[i])
.then((value) => {
res[i] = value;
// 错误的写法
// if (res.length === arr.length) {
// resolve(res);
// }
// 正确写法
count ++
if(count === arr.length) {
resolve(res);
}
})
.catch((reason) => {
reject(reason);
})
} })
}
最后一个点,其他代码都写对了,最后一步resolv(res),写错位置了,这个是最过分的。
Promise.all = (arr) => {
return new Promise((resolve, reject) => {
let res = [];
let count = 0;
for (let i = 0; i < arr.length; i++) {
Promise.resolve(arr[i])
.then((value) => {
res[i] = value;
count ++
// 正确写法
if(count === arr.length) {
resolve(res);
}
})
.catch((reason) => {
reject(reason);
})
}
// 错误写法
// if(count === arr.length) {
// resolve(res);
// }
})
}
最后如果将if判断写在了for循环底下,跟这个错误比起来以上说到的点都算是小问题,这一步如果写到for外面,那么就可以理解为,你对同步以及异步编程没有任何的理解。
为什么这么说,因为整体代码for循环也好,Promise.resolve也好都是同步执行,而所谓的异步只不过是在.then和.catch的回调里面,如果将resolve(res)写到for循环低下,当for循环执行完成后,直接就会到这一步,根本不会resolve一个数据出去,最终的结果就是一个undefined。
最后结果代码很少,但是考察的点非常多。
Promise.all = (arr) => {
return new Promise((resolve, reject) => {
let res = [];
let count = 0;
arr.forEach((item, i) => {
Promise.resolve(item)
.then(value => {
res[i] = value;
count++;
if(count === arr.length) resolve(res)
})
.catch(reason => {
reject(reason);
})
})
})
}
Promise.prototype.finally
和.all不一样的是,.finally是真的挂载在原型对象上的,是一个实例方法,是一个函数接收一个回调方法。
Promise.prototype.finally特性
-
无论promise被reslove或者reject,都会执行到finally里面去
this.then里面要接收两个参数,一个参数是是被resolve的返回值,一个是被reject的回调
Promise.prototype.finally = (callback) => {
return this.then(
(value) => {
return Promise.resolve(callback()).then(() => value)
},
(error) => {
return Promise.reject(callback()).then(() => { throw error })
}
)
}