【问题标题】:JavaScript: having trouble to implement my own Promise.all with iterableJavaScript:无法用可迭代实现我自己的 Promise.all
【发布时间】:2021-11-30 14:02:03
【问题描述】:

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

但挑战在于如何遍历可迭代对象。当前的实现是通过const length = promiseArray.length 获取if 的长度并使用for 循环执行它,但是只有数组具有length 属性,其他可迭代对象或迭代器如SetMap.values() 将没有该属性可用。

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

【问题讨论】:

  • Symbol.iterator 确定for( ... of ... ) 是否有效,所以我想说只是利用它。无需测试符号,只需尝试使用try { for( of ) {}} catch (err) { ... } 运行您想要运行的代码。你可以记录你在 for 循环中处理了多少事情,不需要 .length 属性。但是,您确实需要返回一个纯数组,其顺序与您开始(未解决)每个承诺时的顺序相同(根据@derpirscher 的评论)。
  • 你的方法有一个更大的问题:一旦解决,Promise.all 必须返回一个包含与输入承诺相同顺序的结果的迭代。使用您的方法,您只是在结果可用时推送结果。因此,顺序可能是完全随机的......

标签: javascript promise


【解决方案1】:

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

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

[...]

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

  • 类数组对象(具有length 属性和索引元素的对象);或
  • iterable objectsMapSet 等对象)。

这也可能有助于为您的输入提供明确的索引,这(正如 derpirsher 在 cmets 中提到的)对于确保您的 all 实现按顺序返回结果(即使它们可能完成)是必要的乱七八糟的。

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

【讨论】:

  • 为什么要使用Array.from(foo) 而不是[...foo]
  • 没有强烈的个人原因;更彻底的答案是available on SO[...foo] 不适用于带有length 的类似数组的对象,尽管不可否认Promise.all 也不适用。 Array.from 对 polyfill 更友好,尽管 Promises 和 spread 元素在 ES6/ES2015 中都是标准化的,所以它们都是长期标准并且可能是公平的游戏。 TBH 我想我只是把Array.from 作为可迭代对象和非数组length 包含对象(如NodeList)的缓冲区。
  • 但是 [...foo] 可以与 NodeList 一起使用 :) 试试 [...document.querySelectorAll('div')] - 它可以胜任!
  • 它确实...在现代 Chrome 上:) 但只是因为 NodeList iterability 是在 Chrome 51 (2016-05) 中添加的,在 the introduction of Array.from in Chrome 45 (2015-09) 后面是 9 months。边缘didn't even support NodeList.forEach until Edge 16 (2017-10-08)。在 2019 年改用 Chromium 之前,它是 unclear if Edge ever got support
  • @JeffBowman - 描述了for/of nodeList 或 HTMLCollection 的可迭代性的历史here。 Chrome 和 Firefox 于 2016 年 12 月拥有它。2017 年 12 月,Edge 拥有它用于 nodeList,但没有 HTMLCollection。到 2018 年 11 月,Edge 终于将它用于 HTMLCollection(在他们过渡到 Chromium 之前)。
【解决方案2】:

但挑战在于如何遍历可迭代对象。

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

当前的实现是通过 const length = promiseArray.length 使用 for 循环获取 if 的长度,但是只有数组具有长度属性,其他迭代器或迭代器(如 Set 或 Map.values() 将没有该属性可用。

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


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

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

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

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

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

同样,我们需要产生作为 promise 的解析值,我们返回一个与原始 promise 顺序相同的结果数组。

而且,iterable 不一定是所有的 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 的 ECMAScript 规范中看到其中的一些逻辑。另外,请务必查看PerformPromiseAll

【讨论】:

  • @joji - 这对你有用吗?你有没有按照我的做法做不同的事情?
猜你喜欢
  • 1970-01-01
  • 2023-01-28
  • 2021-10-24
  • 2014-01-19
  • 2013-11-02
  • 2011-02-09
  • 2010-09-13
  • 2020-07-27
  • 2020-08-10
相关资源
最近更新 更多