本文通过讲解JavaScript Promise迷你书的前两章有关Promise的关键知识来解析一道常考的promise面试题-手动实现Promise.all。
第一章主要介绍了如何编写promise代码:
第二章为promise实战延伸,只提取了3个常用的知识点:
可能光是看知识点不能完全理解,下面会结合手动实现Promise.all来应用上面的知识点。首先看一下promise.all的使用示例:
var p1 = Promise.resolve(1),
p2 = Promise.resolve(2),
p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(function (results) {
console.log(results); // [1, 2, 3]
});
Promise.all接收一个promise数组,返回一个promise实例,then回调函数参数为promise满足返回值的数组,与Promise.all接收的promise实例相对应。如果有一个promise变成reject状态,就执行catch方法:
var p1 = Promise.resolve(1),
p2 = Promise.reject(2),
p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(function (results) {
//then方法不会被执行
console.log(results);
}).catch(function (e){
//catch方法将会被执行,输出结果为:2
console.log(2);
});
Promise.all返回的是一个promise实例,所以我们手写的过程中也要返回一个promise,利用promise迷你书中实例化Promise的方法:
Promise.myall = function(promises){
return new Promise(fn)
}
接下来我们再来处理promise数组promises,这里我们应该如何来获取promise实例变成fullfilled状态后的返回值呢?我们可以通过promise实例的then方法注册满足状态的回调函数,来获取满足状态的返回值。我们可以通过for循环的方式通过执行promise.then的方式来等待所有promise实例变化成resolve状态。此处还用到了闭包的知识点,在for循环中,如果i较小的promise居后返回了,在for的循环作用域中仍然可以记忆当时的i序号,返回主体为promise,结果数组变量在这个promise的构造过程中声明就好:
Promise.myall = function(promises){
return new Promise(function(resolve, reject){
let promiseNum = promises.length;
let resolvedValues = [];
for(let i = 0; i < promiseNum; i++){
promises[i].then(function(res){
resolvedValues[i] = res;
})
}
})
}
接下来我们需要判断promise全部返回的时机,可以在for循环中通过结果i序号与输入promises数组长度对比来判断么?不可以,因为这样只要for循环完就会使我们的myall返回的promise变成fullfilled状态,不管所有异步promise是否都已经返回。所以我们可以把判断的逻辑放在每一个promise状态变成fullfilled的回调函数中来进行判断,这样就可以与promise真正变化状态的时机联系起来。但是如果还是判断序号i与promises长度还是不可以,因为如果最后一个promise先返回,那么依然会存在误判。所以我们通过序号i来判断并不准确,最准确的判断条件还是需要用在每一个promise状态变化之后的回调函数中来计数才比较安全:
Promise.myall = function(promises){
return new Promise(function(resolve, reject){
let promiseNum = promises.length;
let resolvedValues = [];
let resolvedCounter = 0;
for(let i = 0; i < promiseNum; i++){
promises[i].then(function(res){
resolvedCounter++;
resolvedValues[i] = res;
if (resolvedCounter == promiseNum) {
return resolve(resolvedValues);
}
})
}
})
}
下面我们还要考虑如果有一个promise变成rejected的状态,我们需要在promise构造函数的函数参数中调用reject的,很简单只需要在then的第二个参数来加上执行reject函数的逻辑即可:
Promise.myall = function(promises){
return new Promise(function(resolve, reject){
let promiseNum = promises.length;
let resolvedValues = [];
let resolvedCounter = 0;
for(let i = 0; i < promiseNum; i++){
promises[i].then(function(res){
resolvedCounter++;
resolvedValues[i] = res;
if (resolvedCounter == promiseNum) {
return resolve(resolvedValues);
}
}, function(reason){
return reject(reason);
})
}
})
}
至此一个基本的Promise.myall就实现好了,但是有的面试管这时还会来一个附加题:Promise.all是所有的promise同时开始执行,但是如果想让上一个promise执行完再执行下一个promise又该如何实现?
可能最先想到的是用await对上面的结果进行修改。但是我们也可以通过promise chain的方式来思考,下面是promise迷你书中介绍的一种方法:
function main(){
function recordValue(results, value){
results.push(value);
return results;
}
var pushValue = recordValue.bind(null, []);
var tasks = [promise1, promise2];
var promise = Promise.resolve();
for(var i = 0; i < tasks.length; i++){
var task = function(){
tasks[i].then((res)=>{
return res;
});
}
promise = promise.then(task).then(pushValue);
}
return promise;
}
main().then(function(value){
console.log(value);
}).catch(function(error){
console.error(error);
})
可以简单分析一下,摘抄下来的时候有一些改动。首先再for循环前面通过Promise.resolve()来初始化一个fullfilled状态的promise实例,将需要迭代的每个promise放到了一个函数中,即为上一个promisefullfilled状态的回调函数,在回调函数中返回结果,经过promise迷你书中所述的then中回调函数返回值会经过Promise.resolve(返回值)包装,所以链式调用then时,会把返回的结果直接传入pushValue中,而pushValue处理完了上个结果后,会返回一个新的promise,下次迭代会基于这个新的已经fullfilled的promise直接执行满足状态的回调,最后返回的是所有promise数组返回的结果。
参考: 《JavaScript Promise 迷你书》
您是否也面临不知道自己在行业内技术水平如何,遇到的技能点由于长期没有再接触过容易忘记的问题呢?欢迎访问刷题小程序来解决上述两点问题,每月刷题最多的同学可以任选100元以内图书一本哦~