【问题标题】:Filtering an array with a function that returns a promise使用返回承诺的函数过滤数组
【发布时间】:2016-01-26 03:06:41
【问题描述】:

给定

let arr = [1,2,3];

function filter(num) {
  return new Promise((res, rej) => {
    setTimeout(() => {
      if( num === 3 ) {
        res(num);
      } else {
        rej();
      }
    }, 1);
  });
 }

 function filterNums() {
   return Promise.all(arr.filter(filter));
 }

 filterNums().then(results => {
   let l = results.length;
   // length should be 1, but is 3
 });

长度为 3,因为返回的是 Promise,而不是值。有没有办法使用返回 Promise 的函数过滤数组?

注意:本例中 fs.stat 已替换为 setTimeout,具体代码见https://github.com/silenceisgolden/learn-esnext/blob/array-filter-async-function/tutorials/array-filter-with-async-function.js

【问题讨论】:

  • “有没有办法使用返回 Promise 的函数来过滤数组?” 使用Array#filter 肯定不行。
  • @FelixKling 这也是我的结论,但你能进一步解释背后的原因吗?我不明白为什么会这样。这对我来说似乎是半合乎逻辑的。
  • 因为过滤器需要一个返回布尔值而不是承诺对象的函数
  • @JonahWilliams 是的,我明白这一点。将过滤器函数更改为异步函数会产生相同的结果,所以我猜这也会返回一个承诺,而不是等待返回的布尔值。

标签: javascript arrays ecmascript-6 es6-promise


【解决方案1】:

一个有效的方法来做到这一点(但它似乎太乱了):

let arr = [1,2,3];

function filter(num) {
  return new Promise((res, rej) => {
    setTimeout(() => {
      if( num === 3 ) {
        res(num);
      } else {
        rej();
      }
    }, 1);
  });
}

async function check(num) {
  try {
    await filter(num);
    return true;
  } catch(err) {
    return false;
  }
}

(async function() {
  for( let num of arr ) {
    let res = await check(num);
    if(!res) {
      let index = arr.indexOf(num);
      arr.splice(index, 1);
    }
  }
})();

再一次,看起来太乱了。

【讨论】:

  • 仅供参考 async/await 关键字是 ES7(候选)而不是 ES6
【解决方案2】:

如 cmets 中所述,Array.prototype.filter同步的,因此不支持 Promises。

既然您现在(理论上)可以使用 ES6 子类化内置类型,您应该能够添加自己的异步方法来包装现有的过滤器函数:

注意:我已经注释掉了子类化,因为 Babel 还不支持 Arrays

class AsyncArray /*extends Array*/ {
  constructor(arr) {
    this.data = arr; // In place of Array subclassing
  }

  filterAsync(predicate) {
     // Take a copy of the array, it might mutate by the time we've finished
    const data = Array.from(this.data);
    // Transform all the elements into an array of promises using the predicate
    // as the promise
    return Promise.all(data.map((element, index) => predicate(element, index, data)))
    // Use the result of the promises to call the underlying sync filter function
      .then(result => {
        return data.filter((element, index) => {
          return result[index];
        });
      });
  }
}
// Create an instance of your subclass instead
let arr = new AsyncArray([1,2,3,4,5]);
// Pass in your own predicate
arr.filterAsync(async (element) => {
  return new Promise(res => {
    setTimeout(() => {
      res(element > 3);
    }, 1);
  });
}).then(result => {
  console.log(result)
});

Babel REPL Demo

【讨论】:

  • 这是无效的,因为super()必须在constructor内的this的任何赋值之前调用
  • @FarzadYZ 子类的实现只是一个例子。您不需要具有真正子类化的构造函数,因为您将使用基本 Array 构造函数而不使用您自己的数据存储
  • 你说得对,我只是警告那些盲目复制粘贴已接受答案的人:)
  • @FarzadYZ 好点,看起来你可以取消注释该块并让它工作......
【解决方案3】:

这是一种方法:

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));
var filter = num => wait(1).then(() => num == 3);

var filterAsync = (array, filter) =>
  Promise.all(array.map(entry => filter(entry)))
  .then(bits => array.filter(entry => bits.shift()));

filterAsync([1,2,3], filter)
.then(results => console.log(results.length))
.catch(e => console.error(e));

filterAsync 函数接受一个数组和一个函数,该函数必须返回 truefalse 或返回解析为 truefalse 的承诺,这是您所要求的(几乎,我没有t 超载承诺拒绝,因为我认为这是一个坏主意)。如果您对此有任何疑问,请告诉我。

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));
var filter = num => wait(1).then(() => num == 3);

var filterAsync = (array, filter) =>
  Promise.all(array.map(entry => filter(entry)))
  .then(bits => array.filter(entry => bits.shift()));

filterAsync([1,2,3], filter)
.then(results => console.log(results.length))
.catch(e => console.error(e));

var console = { log: msg => div.innerHTML += msg + "<br>",
                error: e => console.log(e +", "+ (e.lineNumber-25)) };
&lt;div id="div"&gt;&lt;/div&gt;

【讨论】:

    【解决方案4】:

    Promise Reducer 来救援!

    [1, 2, 3, 4].reduce((op, n) => {
        return op.then(filteredNs => {
            return new Promise(resolve => {
                setTimeout(() => {
                    if (n >= 3) {
                        console.log("Keeping", n);
                        resolve(filteredNs.concat(n))
                    } else {
                        console.log("Dropping", n);
                        resolve(filteredNs);
                    }
                }, 1000);
            });
        });
    }, Promise.resolve([]))
    .then(filteredNs => console.log(filteredNs));
    

    减速器很棒。 “将我的问题减少到我的目标”似乎是一个非常好的策略,对于比简单工具为您解决的更复杂的事情,即过滤一系列并非立即可用的事情。

    【讨论】:

      【解决方案5】:

      游戏晚了,但由于没有其他人提到它,Bluebird 支持 Promise.map,这是我的首选过滤器,需要针对条件进行 aysnc 处理,

      function filterAsync(arr) {
          return Promise.map(arr, num => {
              if (num === 3) return num;
          })
              .filter(num => num !== undefined)
      }
      

      【讨论】:

      【解决方案6】:

      这是 2017 年使用 async/await 的优雅解决方案:

      非常简单的用法:

      const results = await filter(myArray, async num => {
        await doAsyncStuff()
        return num > 2
      })
      

      辅助函数(将其复制到您的网页中):

      async function filter(arr, callback) {
        const fail = Symbol()
        return (await Promise.all(arr.map(async item => (await callback(item)) ? item : fail))).filter(i=>i!==fail)
      }
      

      演示:

      // Async IIFE
      (async function() {
        const myArray = [1, 2, 3, 4, 5]
      
        // This is exactly what you'd expect to write 
        const results = await filter(myArray, async num => {
          await doAsyncStuff()
          return num > 2
        })
      
        console.log(results)
      })()
      
      
      // Arbitrary asynchronous function
      function doAsyncStuff() {
        return Promise.resolve()
      }
      
      
      // The helper function
      async function filter(arr, callback) {
        const fail = Symbol()
        return (await Promise.all(arr.map(async item => (await callback(item)) ? item : fail))).filter(i=>i!==fail)
      }

      我什至会投一个CodePen

      【讨论】:

      • 这与普通 Array.filter 的行为有细微的差别:如果您尝试以 包含 未定义元素的方式过滤数组,您将丢失它们。例如。 filter([1, 2, undefined, 3], (x) =&gt; x !== 1) 将返回 [2, 3],而不是应有的 [2, undefined, 3]
      • @TimPerry 正确,请随时修改答案,以便更有意义:)
      • 一种选择是返回 Symbol 标记值而不是 undefined
      • @Tamlyn 添加了符号哨兵来修复undefined 的情况:)
      • 可能这个函数应该被重命名为不同的东西,比如filterAsync。我承认,在我草率的 SO-and-paste 工作流程中,只阅读了您答案的第一段,看到您使用了filter(),并假设Array.filter 将支持异步回调...?
      【解决方案7】:

      @DanRoss 的变体:

      async function filterNums(arr) {
        return await arr.reduce(async (res, val) => {
          res = await res
          if (await filter(val)) {
            res.push(val)
          }
          return res
        }, Promise.resolve([]))
      }
      

      请注意,如果(在当前情况下)您不必担心 filter() 有 需要序列化的副作用,也可以这样做:

      async function filterNums(arr) {
        return await arr.reduce(async (res, val) => {
          if (await filter(val)) {
            (await res).push(val)
          }
          return res
        }, Promise.resolve([]))
      }
      

      【讨论】:

        【解决方案8】:

        对于打字稿人(或 es6 只需删除类型语法)

        function mapAsync<T, U>(array: T[], callbackfn: (value: T, index: number, array: T[]) => Promise<U>): Promise<U[]> {
          return Promise.all(array.map(callbackfn));
        }
        
        async function filterAsync<T>(array: T[], callbackfn: (value: T, index: number, array: T[]) => Promise<boolean>): Promise<T[]> {
          const filterMap = await mapAsync(array, callbackfn);
          return array.filter((value, index) => filterMap[index]);
        }
        

        es6

        function mapAsync(array, callbackfn) {
          return Promise.all(array.map(callbackfn));
        }
        
        async function filterAsync(array, callbackfn) {
          const filterMap = await mapAsync(array, callbackfn);
          return array.filter((value, index) => filterMap[index]);
        }
        

        es5

        function mapAsync(array, callbackfn) {
          return Promise.all(array.map(callbackfn));
        }
        
        function filterAsync(array, callbackfn) {
          return mapAsync(array, callbackfn).then(filterMap => {
            return array.filter((value, index) => filterMap[index]);
          });
        }
        

        编辑:演示

        function mapAsync(array, callbackfn) {
          return Promise.all(array.map(callbackfn));
        }
        
        function filterAsync(array, callbackfn) {
          return mapAsync(array, callbackfn).then(filterMap => {
            return array.filter((value, index) => filterMap[index]);
          });
        }
        
        var arr = [1, 2, 3, 4];
        
        function isThreeAsync(number) {
          return new Promise((res, rej) => {
            setTimeout(() => {
              res(number === 3);
            }, 1);
          });
        }
        
        mapAsync(arr, isThreeAsync).then(result => {
          console.log(result); // [ false, false, true, false ]
        });
        
        filterAsync(arr, isThreeAsync).then(result => {
          console.log(result); // [ 3 ]
        });

        【讨论】:

        • 这是一个很好的答案。我所做的唯一调整是将readonly 添加到数组参数类型中。
        • 需要演示如何调用它
        【解决方案9】:

        如果有人对现代打字稿解决方案感兴趣(带有用于过滤的失败符号):

        const failSymbol = Symbol();
        
        export async function filterAsync<T>(
          itemsToFilter: T[],
          filterFunction: (item: T) => Promise<boolean>,
        ): Promise<T[]> {
          const itemsOrFailFlags = await Promise.all(
            itemsToFilter.map(async (item) => {
              const hasPassed = await filterFunction(item);
        
              return hasPassed ? item : failSymbol;
            }),
          );
        
          return itemsOrFailFlags.filter(
            (itemOrFailFlag) => itemOrFailFlag !== failSymbol,
          ) as T[];
        }
        

        【讨论】:

          【解决方案10】:

          asyncFilter 方法:

          Array.prototype.asyncFilter = async function(f){
              var array = this;
              var booleans = await Promise.all(array.map(f));
              return array.filter((x,i)=>booleans[i])
          }
          

          【讨论】:

          • 只是想知道,如果任何回调拒绝承诺/抛出错误,它会起作用吗?
          【解决方案11】:

          你可以做这样的事情......

          theArrayYouWantToFilter = await new Promise(async (resolve) => {
            const tempArray = [];
          
            theArrayYouWantToFilter.filter(async (element, index) => {
              const someAsyncValue = await someAsyncFunction();
          
              if (someAsyncValue) {
                tempArray.push(someAsyncValue);
              }
          
              if (index === theArrayYouWantToFilter.length - 1) {
                resolve(tempArray);
              }
            });
          });
          

          包装在异步函数中...

          
          async function filter(theArrayYouWantToFilter) {
            theArrayYouWantToFilter = await new Promise(async (resolve) => {
              const tempArray = [];
          
              theArrayYouWantToFilter.filter(async (element, index) => {
                const someAsyncValue = await someAsyncFunction();
          
                if (someAsyncValue) {
                  tempArray.push(someAsyncValue);
                }
          
                if (index === theArrayYouWantToFilter.length - 1) {
                  resolve(tempArray);
                }
              });
            });
          
            return theArrayYouWantToFilter;
          }
          

          【讨论】:

            【解决方案12】:

            聚会迟到了,我知道我的答案与其他已经发布的答案相似,但我要分享的功能已准备好放入任何代码中并可以使用。 像往常一样,当你必须对数组进行复杂的操作时,reduce 才是王道:

            const filterAsync = (asyncPred) => arr => 
              arr.reduce(async (acc,item) => {
                const pass = await asyncPred(item);
                if(pass) (await acc).push(item);
                return acc;
              },[]);
            

            它使用现代语法,因此请确保您的目标支持它。要 100% 正确,您应该使用 Promise.resolve([]) 作为初始值,但 JS 并不关心,这样它会更短。

            那么你可以这样使用它:

            var wait = ms => new Promise(resolve => setTimeout(resolve, ms));
            const isOdd = x => wait(1).then(()=>x%2);
            (filterAsync(isOdd)([1,2,3,4,4])).then(console.log) // => [1,3]
            

            【讨论】:

            • 如果我理解正确,这个版本会依次检查谓词,所以它可能比@GabeRogan 的并行map() 慢,我认为这是最好的(应该是公认的答案)。
            【解决方案13】:

            这是@pie6k 的 Typescript 版本的较短版本:

            async function filter<T>(arr: T[], callback: (val: T) => Promise<Boolean>) {
              const fail = Symbol()
              const result = (await Promise.all(arr.map(async item => (await callback(item)) ? item : fail))).filter(i => i !== fail)
              return result as T[] // the "fail" entries are all filtered out so this is OK
            }
            

            【讨论】:

            • 你应该在这里使用类型保护而不是类型断言。 .filter((i): i is Awaited&lt;T&gt; =&gt; i !== fail);
            【解决方案14】:

            出于生产目的,您可能希望使用像 lodasync 这样的库:

            import { filterAsync } from 'lodasync'
            
            const result = await filterAsync(async(element) => {
              await doSomething()
              return element > 3
            }, array)
            

            在底层,它通过在每个元素上调用回调来映射您的数组,并使用结果过滤数组。但你不应该重新发明轮子。

            【讨论】:

              【解决方案15】:

              只有一个班轮可以做到这一点。

              const filterPromise = (values, fn) => 
                  Promise.all(values.map(fn)).then(booleans => values.filter((_, i) => booleans[i]));
              

              将数组传递给values,将函数传递给fn

              here 提供了有关这一班轮工作原理的更多说明。

              【讨论】:

              • 这个挺漂亮的。我需要通过发出异步 HTTP 请求来过滤一组数据,以针对第三方系统验证每一行。这完全解决了。谢谢! (作为记录,类似:filterPromise(rows, (row) =&gt; { return axios.get(blah blah returns a Promise).then( data =&gt; { return data.passes.the.boolean.test; }) }).then( resultRows =&gt; { resolve(resultRows); }) ..(在我的情况下,我已经在一个异步函数中,该函数有一个 resolve 函数,可以解析我的函数返回的 Promise)。希望对某人有所帮助:)跨度>
              • 编辑:最后使用了lodasync,请参阅下面 Keller 的帖子,但仍然很高兴看到算法的所有美丽:)
              【解决方案16】:

              两行,完全类型安全

              export const asyncFilter = async <T>(list: T[], predicate: (t: T) => Promise<boolean>) => {
                const resolvedPredicates = await Promise.all(list.map(predicate));
                return list.filter((item, idx) => resolvedPredicates[idx]);
              };
              

              【讨论】:

                【解决方案17】:

                解决此问题的一种有效方法是将数组作为可迭代对象处理,因此您可以在一次迭代中应用任意数量的所需操作。

                以下示例为此使用库 iter-ops

                import {pipe, filter, toAsync} from 'iter-ops';
                
                const arr = [1, 2, 3]; // synchronous iterable
                
                const i = pipe(
                    toAsync(arr), // make our iterable asynchronous
                    filter(async (value, index) => {
                        // returns Promise<boolean>
                    })
                );
                
                (async function() {
                    for await (const a of i) {
                        console.log(a); // print values
                    }
                })();
                

                库中的所有运算符在异步管道中都支持异步谓词(为什么我们使用toAsync),您可以以相同的方式添加其他运算符。

                为此使用Promise.all 效率非常低,因为您会阻止整个数组进行任何可以同时进行的进一步处理,而上述方法允许这样做。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2017-05-11
                  • 1970-01-01
                  • 2016-10-24
                  • 2017-04-05
                  相关资源
                  最近更新 更多