开始今天的面试:请实现 promise.all

489 阅读3分钟

写在前面

promise是面试中经常被考查的知识点,最近,面试官好像更热衷于考查Promise的相关api的实现,比如promise.all、 race、any、allSettled的实现原理。

我们今天就来逐个拆解一下,做到有备无患!

理论准备

Promise.all() 方法接收一个 promise 的 iterable 类型(注:Array,Map,Set 都属于 ES6 的 iterable 类型)的输入,并且只返回一个Promise实例, 那个输入的所有 promise 的 resolve 回调的结果是一个数组。这个Promise的 resolve 回调执行是在所有输入的 promise 的 resolve 回调都结束,或者输入的 iterable 里没有 promise 了的时候。它的 reject 回调执行是,只要任何一个输入的 promise 的 reject 回调执行或者输入不合法的 promise 就会立即抛出错误,并且 reject 的是第一个抛出的错误信息。

提炼一下重点信息:

  • 返回值是一个promise
  • 如果全部promise都成功resolve,状态变为fulfilled,会按照传入顺序返回三者的resolve结果。
  • 如果其中有任何一个reject,则直接返回这个reject结果。

动手实践

  • 既然返回是个 promise,那我们先写个promise占位:
Promise.myAll = (args) => {
    return new Promise((resolve, reject) => {
        
        resove([])
    })
}
  • 如果传入promise都成功,则返回三者的resolve结果数组,而且顺序要不变,那么我们遍历执行传入的promise:

定义一个数组作为最后的返回promise的resove结果。每一个promise的执行结果存放在这个数组的对应index位置。

Promise.myAll = (args) => {
    return new Promise((resolve, reject) => {
       + const resArr = [];
       + args.forEach((item, index) => {
       +    item.then((res) => {
       +        resArr[index] = res;
               if(index === args.length - 1) resolve(resArr);
       +   })
       })
       
    })
}

虽然执行会按照遍历顺序,但是返回结果是异步的,也可能最后一个先返回,这时满足了index === args.length - 1的条件,但是数组其他项都是empty,所以我们需要引入额外的参数来记录已经resolve的promise.

加入count记录已经resolve的promise数量。

Promise.myAll = (args) => {
    return new Promise((resolve, reject) => {
       const resArr = [];
       let count = 0;
       args.forEach((item, index) => {
          item.then((res) => {
              count += 1;
              resArr[index] = res;
              if(count === args.length) resolve(resArr);
          })
       })
       
    })
}
  • 如果有任何一个reject,则返回这个reject结果,在执行传入的promise过程中,碰到reject就直接reject,不管其他promise的执行状态。
Promise.myAll = (args) => {
    return new Promise((resolve, reject) => {
       const resArr = [];
       let count = 0;
       args.forEach((item, index) => {
          item.then((res) => {
              count += 1;
              resArr[index] = res;
              if(count === args.length) resolve(resArr);
          }).catch(err => {
              reject(err);
          })
       })
       
    })
}

优化一下,当传入的参数不是可迭代对象时,报错。

如何判断是否是可迭代对象呢?

要成为可迭代对象, 一个对象必须实现  @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性

ok, 有了Mdn的这条理论,我们很容易实现:

Promise.myAll = (args) => {
    return new Promise((resolve, reject) => {
       if(!args[Symbol.iterator]) reject(new Error('arguments is not interator!'))
       const resArr = [];
       let count = 0;
       args.forEach((item, index) => {
          item.then((res) => {
              count += 1;
              resArr[index] = res;
              if(count === args.length) resolve(resArr);
          }).catch(err => {
              reject(err);
          })
       })
    })
}

代码测试

我们准备三个简单的promise来验证我们的结果:

const p1 = new Promise((res,rej)=>{ setTimeout(()=>{ res('promise1 成功返回!') },1000) }) 
const p2 = new Promise((res,rej)=>{ setTimeout(()=>{ res('promise2 成功返回!') },2000) })
const p3 = new Promise((res,rej)=>{ setTimeout(()=>{ rej('promise3 reject!') },3000) })

我们先看看ES6的promise.all的执行结果:

Promise.all([p1, p2]);
Promise.all([p1, p2, p3]);

image.png

我们代码的结果:

image.png

image.png 传入两个成功的promise返回ok,增加一个失败的也是ok的。

我们再来测试一下非数组传入的情况:

image.png

nice!!!看上去还是挺完美的~

写在最后

更进一步的话对数组的每一项是否是promise对象也加一层判断,或者用Promise.resolve包裹转换成promise对象。

这里就不在扩展了,提供一个思路,感觉这个判断方法还蛮有用的,记录一下。

判断是否是promise对象,可以参考vue-next源码的shared模块里的实现方案: [github.com/vuejs/vue-n…link.zhihu.com/?target=htt…

Promise对象: 有.then和catch方法且都是function的对象

const isPromise = (val) => {
    return isObject(val) && isFunction(val.then) && isFunction(val.catch);
};