本文通过讲解JavaScript Promise迷你书的前两章有关Promise的关键知识来解析一道常考的promise面试题-手动实现Promise.all。 第一章主要介绍了如何编写promise代码:
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(let 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 迷你书》
https://juejin.im/post/6844903509934997511
您是否也面临不知道自己在行业内技术水平如何,遇到的技能点由于长期没有再接触过容易忘记的问题呢?欢迎访问刷题小程序来解决上述两点问题,每月刷题最多的同学可以任选100元以内图书一本哦~