字节飞书面试 - 请实现 Promise.all

发布于 2023-09-21 08:20:00 字数 11235 浏览 35 评论 0

何为 Promise.all?

Promise.all 是 es6 Promise 对象上的一个方法,它的功能就是将多个 Promise 实例包装成一个 promise 实例。以下是 MDN 对 Promise.all 的描述:

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

我戴上我的 300 度近视眼镜,仔细地提取出这段描述中的关键字

  1. Promise.all 的返回值是一个新的 Promise 实例。
  2. Promise.all 接受一个可遍历的数据容器,容器中每个元素都应是 Promise 实例。咱就是说,假设这个容器就是数组。
  3. 数组中每个 Promise 实例都成功时(由 pendding 状态转化为 fulfilled 状态), Promise.all 才成功。这些 Promise 实例所有的 resolve 结果回按原来的顺序集合在一个数组中作为 Promise.allresolve 的结果。
  4. 数组中只要有一个 Promise 实例失败(由 pendding 状态转化为 rejected 状态), Promise.all 就失败。 Promise.all.catch() 会捕获到这个 reject

原生 Promise.all 测试

咱先看看原生的 Promise.all 的是啥效果。

const p1 = Promise.resolve('p1')

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延时一秒')
  }, 1000)
})

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延时两秒')
  }, 2000)
})

const p4 = Promise.reject('p4 rejected')

const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延时 1.5 秒')
  }, 1500)
})

// 所有 Promise 实例都成功
Promise.all([p1, p2, p3])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // 2 秒后打印 [ 'p1', 'p2 延时一秒', 'p3 延时两秒' ]
  
// 一个 Promise 实例失败
Promise.all([p1, p2, p4])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // p4 rejected
  
// 一个延时失败的 Promise
 Promise.all([p1, p2, p5])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // 1.5 秒后打印 p5 rejected
  
// 两个 Promise 实例失败
Promise.all([p1, p4, p5])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // p4 rejected

注意

上面 p4p5 在未传入 Promise.all 时需要注释掉,因为一个调用了 rejectPromise 实例如果没有使用 .catch() 方法去捕获错误会报错。但如果 Promise 实例定义了自己的 .catch ,就不会触发 Promise.all.catch() 方法。

OK,理论存在,实践开始!

手动实现 Promise.all

Promise.all 接受一个数组,返回值是一个新的 Promise 实例

Promise.MyAll = function (promises) {
  return new Promise((resolve, reject) => {

  })
}

数组中所有 Promise 实例都成功, Promise.all 才成功。不难想到,咱得需要一个数组来收集这些 Promise 实例的 resolve 结果。但有句俗话说得好:“不怕一万,就怕万一”,万一数组里面有元素不是 Promise 咋办 —— 那就得用 Promise.resolve() 把它办了。这里还有一个问题, Promise 实例是不能直接调用 resolve 方法的,咱得在 .then() 中去收集结果。注意要保持结果的顺序。

Promise.MyAll = function (promises) {
  let arr = []
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = res
      })
    }) 
  })
}

将收集到的结果(数组 arr )作为参数传给外层的 resolve 方法。这里咱们肯定是有一个判断条件的,如何判断所有 Promise 实例都成功了呢?新手容易写出这句代码:​

if (arr.length === promises.length) resolve(arr)

咱仔细想想 Promise 使用来干嘛的 —— 处理异步任务。对呀,异步任务很多都需要花时间呀,如果这些 Promise 中最后一个先完成呢?那 arr 数组不就只有最后一项了,前面的所有项都是 empty 。所以这里咱们应该创建一个计数器,每有一个 Promise 实例成功,计数器加一:

Promise.MyAll = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = res
        count += 1
        if (count === promises.length) resolve(arr)
      })
    })
  })
}

最后就是处理失败的情况了,这里有两种写法,第一种是用 .catch() 方法捕获失败:​

Promise.MyAll = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = res
        count += 1
        if (count === promises.length) resolve(arr)
      }).catch(reject)
    })
  })
}

第二种写法就是给 .then() 方法传入第二个参数,这个函数是处理错误的回调函数:

Promise.MyAll = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = res
        count += 1
        if (count === promises.length) resolve(arr)
      }, reject)
    })
  })
}

测试案例

致此 Promise.all 大功告成,赶紧拿来测试一下(摩拳擦掌):

const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延时一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延时两秒')
  }, 2000)
})

const p4 = Promise.reject('p4 rejected')

const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延时 1.5 秒')
  }, 1500)
})

// 所有 Promsie 都成功
Promise.MyAll([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 2 秒后打印 [ 'p1', 'p2 延时一秒', 'p3 延时两秒' ]
  
// 一个 Promise 失败
Promise.MyAll([p1, p2, p4])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p4 rejected
  
// 一个延时失败的 Promise
Promise.MyAll([p1, p2, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 1.5 秒后打印 p5 rejected 延时 1.5 秒
 
// 两个失败的 Promise
Promise.MyAll([p1, p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p4 rejected

与原生的 Promise.all 运行结果不能说很像,只能说一模一样。老话说的好,趁热打铁——正在火候上。我打开某个学习网站(MDN Web Docs \(mozilla.org\)[3]),了解到 Promise 对象用于同时处理多个 Promise 的方法还有 Promise.racePromise.anyPromise.allSettle 。从小老师就教会了咱们举一反三,仔细看了这三个方法的描述之后,我还真给反出来了。

Promise.race

Promise.race 从字面意思理解就是赛跑,以状态变化最快的那个 Promise 实例为准,最快的 Promise 成功 Promise.race 就成功,最快的 Promise 失败 Promise.race 就失败。

咱来看看原生 Promise.race 效果

原生 Promise.race 测试

const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延时一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延时两秒')
  }, 2000)
})

const p4 = Promise.reject('p4 rejected')

const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延时 1 秒')
  }, 1500)
})

// p1 无延时,p2 延时 1s,p3 延时 2s
Promise.race([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1

// p4 无延时 reject
Promise.race([p4, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p4 rejected
  
// p5 延时 1.5 秒 reject,p2 延时 1s
Promise.race([p5, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 1s 后打印: p2 延时一秒

理论存在,实践开始

手写 Promise.race

整体流程与 Promise 差不多,只是对数组中的 Promise 实例处理的逻辑不一样,这里我们需要将最快改变状态的 Promise 结果作为 Promise.race 的结果,相对来说就比较简单了,代码如下:

Promise.MyRace = function (promises) {
  return new Promise((resolve, reject) => {
    // 这里不需要使用索引,只要能循环出每一项就行
    for (const item of promises) {
      Promise.resolve(item).then(resolve, reject)
    }
  })
}

测试案例

还是刚才几个案例,咱就不重复写了

// p1 无延时,p2 延时 1s,p3 延时 2s
Promise.MyRace([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1

// p4 无延时 reject
Promise.MyRace([p4, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p4 rejected
  
// p5 延时 1.5 秒 reject,p2 延时 1s
Promise.MyRace([p5, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 1s 后打印: p2 延时一秒

可以看到,结果与原生的 Promise.race 是一致的,成功!

Promise.any

Promise.anyPromise.all 可以看做是相反的。 Promise.any 中只要有一个 Promise 实例成功就成功,只有当所有的 Promise 实例失败时 Promise.any 才失败,此时 Promise.any 会把所有的失败/错误集合在一起,返回一个失败的 promise 和`AggregateError`[4]类型的实例。MDN 上说这个方法还处于试验阶段,如果 node 或者浏览器版本过低可能无法使用,各位看官自行测试下。

原生 Promise.any 测试

const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延时一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延时两秒')
  }, 2000)
})

const p4 = Promise.reject('p4 rejected')

const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延时 1.5 秒')
  }, 1500)
})

// 所有 Promise 都成功
Promise.any([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1
  
// 两个 Promise 成功
Promise.any([p1, p2, p4])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1

// 只有一个延时成功的 Promise
Promise.any([p2, p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p2 延时 1 秒

// 所有 Promise 都失败
Promise.any([p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // AggregateError: All promises were rejected

可以看出,如果 Promise.any 中有多个成功的 Promise 实例,则以最快成功的那个结果作为自身 resolve 的结果。

OK,理论存在,实践开始

手写 Promise.any

  1. 依葫芦画瓢,咱们先写出 `Promise.any` 的整体结构:
Promise.MyAny = function (promises) {
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {

    })
  })
}

  1. 这里跟`Promise.all` 的逻辑是反的,咱们需要收集 `reject` 的 `Promise`,也需要一个数组和计数器,用计数器判断是否所有的 `Promise` 实例都失败。另外在收集失败的 `Promise` 结果时咱需要打上一个失败的标记方便分析结果。
Promise.MyAny = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(resolve, err => {
        arr[i] = { status: 'rejected', val: err }
        count += 1
        if (count === promises.length) reject(new Error('没有 promise 成功'))
      })
    })
  })
}

这里我没有使用 MDN 上规定的 AggregateError 实例,手写嘛,随心所欲一点,写自己看着舒服的

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

赤濁

暂无简介

文章
评论
26 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文