JavaScript:无法使用 iterable 实现我自己的 Promise.all

JavaScript: having trouble to implement my own Promise.all with iterable

我正在尝试实施我自己的 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

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

The Array.from() static method creates a new, shallow-copied Array instance from an array-like or iterable object.

[...]

Array.from() lets you create Arrays from:

  • array-like objects (objects with a length property and indexed elements); or
  • iterable objects (objects such as Map and Set).

这也可能有助于为您的输入提供明确的索引,这些索引(如 derpirsher 在评论中提到的那样)对于确保您的 all 实施 returns 结果按顺序排列,即使它们可能乱序完成。

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

But the challenge is with how to iterate through the iterable.

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

Current implementation is to get the length of if and doing it with a for loop via const length = promiseArray.length however only arrays have length property on it, other iterables or iterators like Set or Map.values() will not have that property available.

只需计算您在 for/of 次迭代中获得了多少项。


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

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

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

所以,使用 for/of.

似乎是最有效的

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

同样,我们需要生成 promise 的解析值,我们将返回一组与原始 promise 顺序相同的结果。

而且,可迭代对象不一定都是承诺 - 它也可以包含普通值(只是在结果数组中传递)所以我们需要确保我们也使用常规值。我们从原始数组中获得的值包装在 Promise.resolve() 中,以处理在源数组中传递纯值(不是承诺)的情况。

然后,最后,由于我们不能保证有一个 .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() here. Also, make sure to look at PerformPromiseAll.

的 ECMAScript 规范中看到其中的一些逻辑