由一道关于Promise.all的面试题引发的思考

3,862 阅读5分钟

记得点赞再看哦~! 🤨

话不多说,先看以下面试题

使用Promise.all进行5个请求,若其中一个失败了,怎么让其他4个成功返回?🥳

相信绝大多数的同学,看到这道题目的时候都是一脸懵逼的

QQ截图20210824174155.png

但是遇到问题,我们要先保持冷静,下面我们先来看看Promise.all的基本用法

Promise.all的用法

let p1 = Promise.resolve(1)
let p2 = Promise.resolve(2)
let p3 = new Promise((resolve) => {
  setTimeout(() => {
     resolve(3)
  }, 2000);
})

Promise.all([p1, p2, p3]).then((res) => {
  console.log('res', res); // 2s后打印出 [1, 2, 3]
}).catch((err) => {
  console.log('err', err);
})

以上代码在2s后会输出[1, 2, 3];

可见,Promise.all() 方法将一组可迭代的promise作为数组传入,待全部promise都执行成功时,Promise.all就完成了,并返回一个promise,该promise resolve的结果为传入的promise的返回结果组成的数组。

以上是三个promise都执行成功的情况,下面我们看看其中一个失败了,又会输出什么呢?

let p1 = Promise.resolve(1)
let p2 = Promise.reject(2)
let p3 = new Promise((resolve) => {
  setTimeout(() => {
     resolve(3)
  }, 2000);
})

Promise.all([p1, p2, p3]).then((res) => {
  console.log('res', res);
}).catch((err) => {
  console.log('err', err); // 马上打印出 err 2
})

由运行结果可知,当其中一个promise失败时,会马上抛出错误,Promise.all的整体运行失败。

看到这里,有同学就会说了:

QQ截图20210824182319.png

没错,以上关于Promise.all的用法相信绝大多数前端同学都已掌握,但是解决问题,我们还是一步一步来,先了解其用法,再了解原理,这样下来我们就会深刻的掌握。

QQ截图20210824182553.png

Promise.all的实现

在了解了基本用法之后,我们可以自己手撸一个方法来实现Promise.all

基本的实现思路,总结为以下几点:

  1. 接收一个数组类型的参数
  2. 当数组中全部promise执行成功后,返回一个新的promise作为他们的结果集
  3. 只要其中有一个执行失败,状态就变成rejected

顺着这些思路,我们可以大致写出以下方法:

function myAll(arr) {
  let length = arr.length; // 先获取传入的数组的长度
  let currentCount = 0; // 记录当前执行的下标
  let results = []; // 存储返回结果
  return new Promise((resolve, reject) => {
    for (let i = 0; i < arr.length; i++) {
      Promise.resolve(arr[i]).then((res) => {
        results[i] = res; // 存储在results中,下标加一
        currentCount++;
        // 如果执行完全部,返回结果
        if (currentCount === length) {
          resolve(results)
        }
      }).catch((err) => {
        reject(err) // 如果其中一个执行失败,直接reject
      })
    }
    // 如果传入空数组,直接return results
    if(length === 0) {
      resolve(results)
    }
  })
}

经测试,该方法与Promise.all执行结果一致,各位同学可以自行尝试,这里不作过多演示~

(ps: 关于Promise.all的实现,网上存在多个版本,在我看来,只要围绕上面的实现思路进行编写,都是万变不离其宗)

看到这里,有的同学又坐不住了,这跟开头提到那道面试题,又有啥关系呢?

QQ截图20嗖嗖嗖210824215307.png

大家稍安勿躁,答案已经慢慢浮出水面👇

Promise.all的改写

通过观察上面Promise.all的实现,我们可以发现,如果其中一个promise执行失败时,会直接执行reject,外部就会马上从catch中捕捉到错误,直接返回错误

那么,如果我执行失败时,也返回一个结果呢?

下面我们改动一下上面的代码,改动的思路很简单:

当遇到promise执行失败时,不执行reject,直接返回false存储在result中

function myAll(arr) {
  let length = arr.length; // 先获取传入的数组的长度
  let currentCount = 0; // 记录当前执行的下标
  let results = []; // 存储返回结果
  return new Promise((resolve, reject) => {
    for (let i = 0; i < arr.length; i++) {
      Promise.resolve(arr[i]).then((res) => {
        results[i] = res; // 存储在results中
      }).catch((err) => {
        results[i] = false; // 改动:如果其中一个执行失败,返回一个false
      }).finally(() => {
        currentCount++; // 无论成功或失败,下标都加一
        if (currentCount === length) {
          resolve(results)
        }
      })
    }
    // 如果传入空数组,直接return results
    if(length === 0) {
      resolve(results)
    }
  })
}

经过以上改写,我们来测试一下代码

let p1 = Promise.resolve(1)
let p2 = Promise.resolve(2)
let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
     reject(3)
  }, 2000);
})

myAll([p1, p2, p3]).then(res => {
  console.log(res) // 2s后打印出 [1, 2, false]
}).catch(err => {
  console.log('err', err)
})

看到这里,大家应该有一种豁然开朗,茅塞顿开的感觉了

实际上,每个promise,都会在执行成功的时候resolve,执行失败时reject,那么,只要我们在执行失败的时候,也返回一个变量而不执行reject,是不是就可以得到那道面试题的答案了!

QQ截图2021082zzz4221654.png

解答

我们知道,Promise.all是接收一个数组,我们要去掉数组里面每一个promise执行失败时的reject,自然而然我们想到数组的map方法

let p1 = Promise.resolve(1)
let p2 = Promise.resolve(2)
let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
     reject(3)
  }, 2000);
})

let all = Promise.all([p1, p2, p3].map((p) => p.then(res => res).catch(err => false)))

all.then((res) => {
  console.log(res, res.filter(Boolean)) // 2s后打印 [1, 2, false], [1, 2]
}).catch((err) => {
  console.log('err', err)
})

可以看到,整段代码最为关键的部分就是catch里面的处理,没有执行reject而是直接返回了false,从而达到了即使执行失败了,也不被all中的catch捕获到,从而返回所有的结果。

QQ1234截图20210824223302.png

其实,早在ES6的提案中,就有了相关实现的方法👇

关于Promise.allSettled

由于单一 Promise 进入 rejected 状态便会立即让 Promise.all() 的结果进入 rejected 状态,以至于通过 Promise.all() 进入 rejected 状态时,其中的源 Promise 仍然可能处于 pending 状态,以至于无法获得所有 Promise 完成的时机。

于是,新的 Promise.allSettled() API 被提出,其中 settled 状态的定义是非 pending,即 fulfilled 或者 rejected 中的任一状态。它会等待所有源 Promise 进入 fulfilled 或者 rejected 状态。

代码演示:

let p1 = Promise.resolve(1)
let p2 = Promise.resolve(2)
let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
     reject(3)
  }, 2000);
})

Promise.allSettled([p1, p2, p3]).then(res => {
  console.log(res) //2s后打印 [{status: "fulfilled", value: 1}, {status: "fulfilled", value: 2}, {status: "rejected", reason: 3}]
}).catch(err => {
  console.log('err', err)
})

当然,还要对返回的res进行map处理,从而达到我们上面面试题解答中得到的结果,这部分就交给各位同学自行尝试啦~

总结

读完本文后,相信各位同学已经对Promise.all的用法有深刻了解了,下次遇到了有关于Promise.all的情景和题目应该也能得心应手,手到擒来

本文到此就结束啦,有不同的意见及见解欢迎提出~

记得点赞再看哦~!