Promise.all 是什么?请实现 Promise.all
2022年10月6日
💎 加入 E+ 成長計畫 與超過 500+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源
作为前端工程师,在日常的开发中很常用到Promise.all()
,然而你知道该怎么实现这个方法吗? 这是面试很常出现的问题,很多人因为在面试时写不出来而被刷掉,如果你还不知道怎么实现的话,就让我们透过这篇文章一步步实现。
Promise.all() 是什么?
要实现这个方法前,我们要先知道它在做什么。根据MDN 的定义,Promise.all ()
会
- 接收一个内有多个 promises 的
Iterable
,例如 Array、Map、Set。 - 如果
Iterable
是空的,例如空 Array,则 fulfilled 值会是空的 Array。 - 如果
Iterable
不是空的,则如果所有的 promises 都 fulfilled,则依序回传 fulfilled 的值;如果其中有一个 promise 被 rejected,则会马上 reject。
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
// 预期输出结果: Array [3, 42, "foo"]
如何实现简单版的 Promise.all()
在实现完整版之前,我们先以一个仅处理 Array 的 Promise.all
为例子,了解如何实现核心概念,往下再进一步探讨如何处理 Iterable
。我们先直接看代码,看看你能了解多少。有不懂的地方也不担心,下面会透过注解,一行行解释:
function promiseAll(promises) {
if (!Array.isArray(promises)) {
return new TypeError("Arguments must be an array");
}
if (promises.length === 0) {
return Promise.resolve([]);
}
const outputs = [];
let resolveCounter = 0;
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise()
.then((value) => {
outputs[index] = value;
resolveCounter += 1;
if (resolveCounter === promises.length) {
resolve(outputs);
}
})
.catch(reject);
});
});
}
让我们透过以下代码与注解来看如何实现吧:
function promiseAll(promises) {
// 先检查输入是不是 array,如果不是的话就回传错误
if (!Array.isArray(promises)) {
return new TypeError("Arguments must be an array");
}
// 定义中有提到,如果输入是空的,例如空 array,就 resolve 一个空 array
if (promises.length === 0) {
return Promise.resolve([]);
}
// 先宣告一个最终要 resolve 的 outputs,之后每个 promise 被 fulfilled 时,就放到 outputs 里面
const outputs = [];
// 我们需要这个 counter 让我们知道有多少个 promise 已经 fulfilled
let resolveCounter = 0;
// Promise.all() 最终要回传一个 promise
return new Promise((resolve, reject) => {
promises.forEach((promise, index) => {
promise()
.then((value) => {
// 当输入的每个 promise 成功 fulfilled 时,就放到 outputs
// 透过 index,我们可以确保顺序正确
outputs[index] = value;
// 每次成功放入时,counter 要加一
resolveCounter += 1;
// 当 counter 等于 promises 的长度时,代表所有的 promise 都 fulfilled
// 这时最外面的 promise 就可以 resolve
if (resolveCounter === promises.length) {
resolve(outputs);
}
})
.catch(reject); // 如果有任何一个 reject,就直接 reject
});
});
}
实现完整版的 Promise.all()
上面这个版本的Promise.all()
只有处理 Array 这种输入,但实际上的Promise.all()
是能接收所有的Iterable
,因此我们可以进一步优化上面的版本(备注:在面试中能写出上面的版本,基本上要过关是没问题,当然如果想在面试中展现自己的细心度,那么进一步优化是更好的选择。
先想想,如果要处理任意的 Iterable
可以怎么做? 我们可以先判断丢进来的输入是不是可以迭代的,如果不是的话,就提早回传错误。
另外,实际上的 Promise.all
也能处理非 promise 的 Iterable
,例如处理字符串与一般的数组,所以完整版本我们也会进一步处理这问题
const isIterable =
((typeof promises === "object" && promises !== null) ||
typeof promises === "string") &&
typeof promises[Symbol.iterator] === "function";
if (!isIterable) {
return new TypeError("Arguments must be iterable");
}
基本上多了上述步骤的处理,剩下的逻辑就跟 Array 版本的差不多,代码如下 (不同之处会有注解)
function promiseAll(promises) {
// 判断输入是否为 Iterable
const isIterable =
((typeof promises === "object" && promises !== null) ||
typeof promises === "string") &&
typeof promises[Symbol.iterator] === "function";
// 不是的话就回传错误讯息
if (!isIterable) {
return new TypeError("Arguments must be iterable");
}
// 把 Iterable 转成 Array,就可以重复用 Array 版的逻辑
promises = Array.from(promises);
if (promises.length === 0) {
return Promise.resolve([]);
}
const outputs = [];
let resolveCounter = 0;
return new Promise((resolve, reject) => {
// 帮忙处理 resolution 的 helper function
function handleResolution(value, index) {
outputs[index] = value;
resolveCounter += 1;
if (resolveCounter === promises.length) {
resolve(outputs);
}
}
promises.forEach((promise, index) => {
// 这边要检查 promise 是不是 thenable
// 例如 如果 promises 是 [1, 2, 3],这边的 promise 会分别为 1, 2, 3
// 因为不 thenable,我们直接把 1, 2, 3 分别放入 outputs 当中
// 又或者如果 promises 是 "123" 字串,这边会迭代后,最终输出 ["1", "2", "3"]
if (promise.then) {
promise()
.then((value) => handleResolution(value, index))
.catch((e) => reject(e));
} else {
handleResolution(promise, index);
}
});
});
}