JavaScript:很难用iterable实现我自己的Promise.all



我正在尝试实现自己的Promise.all来准备面试。

我的第一个版本是这个

Promise.all = function(promiseArray) {
return new Promise((resolve, reject) => {
try {
let resultArray = []

const length = promiseArray.length 
for (let i = 0; i <length; i++) {
promiseArray[i].then(data => {
resultArray.push(data)

if (resultArray.length === length) {
resolve(resultArray)
}
}, reject)
}
}
catch(e) {
reject(e)
}
})
}

然而,本机Promise.all不仅接受数组,还接受任何可迭代对象,这意味着任何具有Symbol.iterator的对象,例如数组、Set或Map。

因此,类似这样的东西将适用于本机Promise.all,但不适用于我当前的实现

function resolveTimeout(value, delay) {
return new Promise((resolve) => setTimeout(resolve, delay, value))
}

const requests = new Map()
requests.set('reqA', resolveTimeout(['potatoes', 'tomatoes'], 1000))
requests.set('reqB', resolveTimeout(['oranges', 'apples'], 100))
Promise.all(requests.values()).then(console.log); // it works

我修改了我的Promise.all,首先添加了一个检查,看看它是否有Symbol.iterator,以确保它是可迭代的。

Promise.all = function(promiseArray) {
if (!promiseArray[Symbol.iterator]) {
throw new TypeError('The arguments should be an iterable!')
}
return new Promise((resolve, reject) => {
try {

但挑战在于如何在可迭代项中进行迭代。当前的实现是获取if的长度,并通过const length = promiseArray.length使用for循环进行操作。然而,只有数组具有length属性,其他可迭代或迭代器(如SetMap.values())将不具有该属性。

我如何调整我的实现以支持其他类型的可迭代项,如原生Promise.all所做的

但挑战在于如何迭代可迭代项。

只需使用for/of来迭代可迭代项。

当前的实现是获取if的长度,并通过constlength=promiseArray.length使用for循环进行此操作。然而,只有数组上有length属性,其他迭代或迭代器(如Set或Map.values())将没有该属性。

只需计算您在for/of迭代中获得的项目数。


以下是对实现的更详细解释

如果您查看Promise.all()的规范,它接受一个可迭代的作为其参数。这意味着它必须具有适当的迭代器,以便您可以使用for/of对其进行迭代。如果您正在实现规范,则不必检查它是否是可迭代的。如果没有,则实现将throw,并因此以适当的错误拒绝(这是它应该做的)。Promise执行器已经为您捕获异常并拒绝。

正如在其他地方提到的,你可以在iterable上使用Array.from()来从中生成一个实际的数组,但这似乎有点浪费,因为我们并不真正需要数据的副本,我们只需要迭代它。而且,我们将同步迭代它,这样我们就不必担心它在迭代过程中会发生变化。

因此,使用for/of似乎是最有效的。

使用.entries()迭代可迭代项会很好,因为这会给我们提供索引和值,然后我们可以使用索引来知道将该项的结果放入resultArray的何处,但规范似乎不需要在可迭代项上支持.entries(),因此此实现只需使用for (const p of promiseIterable)的简单可迭代项即可。代码使用自己的计数器生成自己的索引,用于存储结果。

同样,我们需要生成一系列结果作为promise的解析值,这些结果的顺序与原始promise相同。

而且,迭代不一定都是promise——它也可以包含纯值(只在结果数组中传递),所以我们需要确保我们也使用正则值。我们从原始数组中获得的值被封装在Promise.resolve()中,以处理在源数组中传递纯值(而不是promise)的情况。

最后,由于我们不能保证具有.length属性,因此没有有效的方法可以提前知道迭代中有多少项。我从两个方面来解决这个问题。首先,我在cntr变量中计算项目,这样当我们完成for/of循环时,我们就知道总共有多少个项目。然后,当.then()的结果出来时,我们可以递减计数器,看看它什么时候达到零,知道什么时候完成。

Promise.all = function(promiseIterable) {
return new Promise((resolve, reject) => {
const resultArray = [];
let cntr = 0;
for (const p of promiseIterable) {
// keep our own local index so we know where this result goes in the
// result array
let i = cntr;

// keep track of total number of items in the iterable
++cntr;

// track each promise - cast to a promise if it's not one
Promise.resolve(p).then(val => {
// store result in the proper order in the result array
resultArray[i] = val;

// if we have all results now, we are done and can resolve
--cntr;
if (cntr === 0) {
resolve(resultArray);
}
}).catch(reject);
}
// if the promiseIterable is empty, we need to resolve with an empty array
// as we could not have executed any of the body of the above for loop
if (cntr === 0) {
resolve(resultArray);
}
});
}

仅供参考,您可以在Promise.all()的ECMAScript规范中看到其中的一些逻辑。此外,请务必查看PerformPromiseAll

最简单、最实用的调整是将迭代结果复制到您控制的数组中。Array.from是一个很好的匹配,就像MDN(强调矿):一样

Array.from()静态方法从类似数组或可迭代对象创建一个新的浅层复制的Array实例。

[…]

Array.from()允许您从以下位置创建数组:

  • 类数组对象(具有length属性和索引元素的对象);或
  • 可迭代对象(诸如MapSet的对象)

这也可能有助于为您的输入提供清晰的索引,这对于确保您的all实现按顺序返回结果(即使它们可能不按顺序完成)是必要的。

当然,当你这样做是为了更好地为面试做准备时,你可能会选择避免使用Array.from等有用的功能,而倾向于在for ( ... of ... )的基础上进行自制实现,以检查你的理解,正如评论中提到的那样。也就是说,一旦担任了真正的开发角色,我希望您能尽可能多地使用Array.from(以及内置的Promise.all)来减少重复和边缘情况。

相关内容

最新更新