在地图中调用异步函数的最佳方法?

Posted

技术标签:

【中文标题】在地图中调用异步函数的最佳方法?【英文标题】:Best way to call an asynchronous function within map? 【发布时间】:2016-01-31 00:10:14 【问题描述】:

我正在映射一个数组,对于新对象的返回值之一,我需要进行异步调用。

var firebaseData = teachers.map(function(teacher) 
  return 
    name: teacher.title,
    description: teacher.body_html,
    image: urlToBase64(teacher.summary_html.match(/src="(.*?)"/)[1]),
    city: metafieldTeacherData[teacher.id].city,
    country: metafieldTeacherData[teacher.id].country,
    state: metafieldTeacherData[teacher.id].state,
    studioName: metafieldTeacherData[teacher.id].studioName,
    studioURL: metafieldTeacherData[teacher.id].studioURL
  
);

该函数的实现类似于

function urlToBase64(url) 
  request.get(url, function (error, response, body) 
    if (!error && response.statusCode == 200) 
      return "data:" + response.headers["content-type"] + ";base64," + new Buffer(body).toString('base64');
    
  );

我不清楚执行此操作的最佳方法是什么……承诺?嵌套回调?在 ES6 或 ES7 中使用一些东西,然后用 Babel 进行编译?

目前最好的实现方式是什么?

【问题讨论】:

也许塔卡看看github.com/caolan/async地图功能 您必须在地图中使用的函数中实现回调或承诺。 顺序运行 Promises(包装器),请参阅this question 的答案 【参考方案1】:

2018 年更新:Promise.all 地图回调中的异步函数更易于实现:

    let firebaseData = await Promise.all(teachers.map(async teacher => 
        return 
            name: teacher.title,
            description: teacher.body_html,
            image: await urlToBase64(teacher.summary_html.match(/src="(.*?)"/)[1]),
            city: metafieldTeacherData[teacher.id].city,
            country: metafieldTeacherData[teacher.id].country,
            state: metafieldTeacherData[teacher.id].state,
            studioName: metafieldTeacherData[teacher.id].studioName,
            studioURL: metafieldTeacherData[teacher.id].studioURL
        
    ));


async function urlToBase64(url) 
  return request.get(url, function (error, response, body) 
    if (!error && response.statusCode == 200) 
      return "data:" + response.headers["content-type"] + ";base64," + new Buffer(body).toString('base64');
    
  );

Edit@2018/04/29:我给大家举个通用的例子:

Edit@2019/06/19 : async/await 应该有 try/catch 来处理错误:如果发生错误会抛出警告信息;

let data = await Promise.all(data.map(async (item) => 
      try 
      item.fetchItem = await fetchFunc(item.fetchParams);

      return item; 
       catch(err) 
         throw err;
      
  ));

【讨论】:

保持地图回调函数异步的意义何在,如果 fetchFunc 返回承诺,那么 Promise.all 应该负责解决所有承诺。我觉得只有当您想按顺序解决所有承诺时才需要它。 .map() 函数将返回Promise 的数组,Promise.all() 将解决该并行问题。该语法可能会将您与return item 混淆,它看起来只是返回一个变量而不是承诺:但该语法等于function() item = data[i]; return new Promise(resolve => fetchFunc().then(result => item.fetchItem = result; resolve(item); ) 谢谢!解决了我的问题。【参考方案2】:

试试下面的异步地图函数amap()

async function amap(arr,fun) 
    return await Promise.all(arr.map(async v => await fun(v)))

或者,写得更简洁:

let amap = async (arr,fun) => await Promise.all(arr.map(async v => await fun(v)))

用法:

let arr = await amap([1,2,3], async x => x*2)
console.log(arr)   // [2, 4, 6]

【讨论】:

【参考方案3】:

到 2020 年,我们现在拥有 for await...of syntax 和 ECMAScript2021,这大大简化了事情:

所以你现在可以简单地这样做了:

//return an array of promises from our iteration:
let promises = teachers.map(async m => 
   return await request.get(....);
);

//simply iterate those
//val will be the result of the promise not the promise itself
for await (let val of promises)
   ....

【讨论】:

ECMAscript 标准机构现在已经把我们带了一个完整的圈子。在 JS 的黑暗早期,我们被困在 for 循环中(或者我们不得不使用 jQuery、Lodash 等来获得 map)。然后是 ES2015,各地的开发者都为终于能够使用地图而欣喜若狂。但是现在我们又回到了for 循环中,至少对于await 代码......前进两步,后退一步:(【参考方案4】:

使用IIFE和Promise.all,可以做一个简单的用例。

await Promise.all(arr.map(el=>(async _=>
    // some async code       
)()))

这个IIFE可以返回一个promise,作为map函数的返回值。

(async _=>
    // some async code       
)()

所以 arr.map 会返回一个 Promise.all 来处理的 Promise 列表。

例子

const sleep = (ms) => 
  return new Promise((resolve, reject) => 
    setTimeout(_ => 
      resolve()
    , ms)
  );


await Promise.all([1000,2000,3000].map(el=>(async _=>
    await sleep(el)
    console.log(el) 
    return el  
)())) 

【讨论】:

【参考方案5】:

如果您想同时映射所有元素:

function asyncMap(arr, fn) 
  return Promise.all(arr.map(fn));

如果您想非并发地映射所有元素(例如,当您的映射函数有副作用或一次在所有数组元素上运行映射器时资源成本太高):

选项 A:承诺

function asyncMapStrict(arr, fn) 
  return new Promise((resolve) => 
    const result = [];
    arr.reduce(
      (promise, cur, idx) => promise
        .then(() => fn(cur, idx, arr)
          .then((res) => 
            result.push(res);
          )),
      Promise.resolve(),
    ).then(() => resolve(result));
  );

选项 B:异步/等待

async function asyncMapStrict(arr, fn) 
  const result = [];

  for (let idx = 0; idx < arr.length; idx += 1) 
    const cur = arr[idx];

    result.push(await fn(cur, idx, arr));
  

  return result;

【讨论】:

在我看来是所有答案中最简洁、最中肯的。【参考方案6】:

我遇到了类似的问题,发现这更容易(我使用的是 Kai 的通用模板)。下面,您只需要使用一个await。我还使用 ajax 函数作为我的异步函数:

function asyncFunction(item) 
    return $.ajax(
        type: "GET",
        url: url,
        success: response => 
            console.log("response received:", response);
            return response;
        , error: err => 
            console.log("error in ajax", err);
        
    );


let data = await Promise.all(data.map(item => asyncFunction(item)));

【讨论】:

【参考方案7】:

通过使用 Promise.all,您可以使 ma​​pforEach 与异步函数(即 Promise)一起工作。

要使 filtersomeevery 工作,您可以首先使用异步映射(反过来使用 Promise.all),然后遍历真/假值并同步进行过滤/评估。

要使 reducereduceRight 与异步函数一起使用,您可以将原始函数包装在一个等待累加器解析的新函数中。

利用这些知识,可以以某种方式修改原始数组方法,以便它们继续“像往常一样”使用普通/同步函数,但也可以使用异步函数。

// a 'mini library' (save it somewhere and import it once/project)
(() => 
  let AsyncFunction = Object.getPrototypeOf(async e => e).constructor;
  ['map', 'forEach'].forEach(method => 
    let orgMethod = Array.prototype[method];
    Array.prototype[method] = function (func) 
      let a = orgMethod.call(this, func);
      return func instanceof AsyncFunction ? Promise.all(a) : a;
    ;
  );
  ['filter', 'some', 'every'].forEach(method => 
    let orgMethod = Array.prototype[method];
    Array.prototype[method] = function (func) 
      if (func instanceof AsyncFunction) 
        return (async () => 
          let trueOrFalse = await this.map(func);
          return orgMethod.call(this, (_x, i) => trueOrFalse[i]);
        )();
      
      else 
        return orgMethod.call(this, func);
      
    ;
  );
  ['reduce', 'reduceRight'].forEach(method => 
    let orgMethod = Array.prototype[method];
    Array.prototype[method] = function (...args) 
      if (args[0] instanceof AsyncFunction) 
        let orgFunc = args[0];
        args[0] = async (...args) => 
          args[0] = await args[0];
          return orgFunc.apply(this, args);
        ;
      
      return orgMethod.apply(this, args);
    ;
  );
)();

// AND NOW:

// this will work
let a = [1, 2, 3].map(x => x * 3); // => [3, 6, 9]
let b = [1, 2, 3, 4, 5, 6, 7].filter(x => x > 3); // [4, 5, 6, 7]
let c = [1, 2, 3, 4, 5].reduce((acc, val) => acc + val); // => 15

// this will also work
let x = await [1, 2, 3].map(async x => x * 3);
let y = await [1, 2, 3, 4, 5, 6, 7].filter(async x => x > 3);
let z = await [1, 2, 3, 4, 5].reduce(async (acc, val) => acc + val);

【讨论】:

你在filter 上的笔记救了我!【参考方案8】:

map 中调用异步函数的最佳方式是使用专门为异步函数创建的map

对于异步函数,它必须返回一个 Promise。

function urlToBase64(url) 
  return new Promise((resolve, reject) => 
    request.get(url, function (error, response, body) 
      if (error) 
        reject(error)
       else if (response && response.statusCode == 200) 
        resolve(
          "data:" + response.headers["content-type"] + ";base64," + new Buffer(body).toString('base64');
        )
       else 
        reject(new Error('invalid response'))
      
    );
  )

现在,我们可以映射:

const  pipe, map, get  = require('rubico')

const metafieldTeacherData =  //  [teacher_id]: ..., ... 

const parseTeacher = teacher => (
  name: teacher.title,
  description: teacher.body_html,
  image: urlToBase64(teacher.summary_html.match(/src="(.*?)"/)[1]),
  city: metafieldTeacherData[teacher.id].city,
  country: metafieldTeacherData[teacher.id].country,
  state: metafieldTeacherData[teacher.id].state,
  studioName: metafieldTeacherData[teacher.id].studioName,
  studioURL: metafieldTeacherData[teacher.id].studioURL
)

const main = async () => 
  const teachers = [] // array full of teachers
  const firebaseData = await map(pipe([
    parseTeacher,
    get('studioURL'),
    urlToBase64,
  ]))(teachers)
  console.log(firebaseData) // > ['data:application/json;base64,...', ...]


main()

rubico 的地图担心Promise.all,所以你不必担心。

【讨论】:

【参考方案9】:

您可以使用async.map。

var async = require('async');

async.map(teachers, mapTeacher, function(err, results)
  // results is now an array of stats for each file
);

function mapTeacher(teacher, done) 
  // computing stuff here...
  done(null, teacher);

请注意,所有教师都将被并行处理 - 您也可以使用以下功能:

mapSeries(arr, iterator, [callback])一一映射

mapLimit(arr, limit, iterator, [callback]) 同时映射limit

【讨论】:

它有一个 lodash 依赖...并不是说这有什么问题。 async@2.6.1 ~ lodash@4.17.11 能够限制同时异步函数的数量至关重要【参考方案10】:

出于生产目的,您可能希望使用像 lodasync 这样的库,您不应该重新发明***:

import  mapAsync  from 'lodasync'

const result = await mapAsync(async(element) => 
  return 3 + await doSomething(element)
, array)

它使用 Promise,没有依赖关系,而且速度很快。

【讨论】:

mapAsync 实现的一个潜在问题是所有元素都将被同时处理。这可能是可取的,也可能不是可取的(例如,您正在访问一个网站,这可能会使服务器过载)。另一个库中描述的异步库有 mapLimit,它允许您限制一次运行多少个函数。【参考方案11】:

为了方便起见,我不得不写这个。否则,我可能需要https://github.com/mcollina/make-promises-safe

export async function mapAsync<T, U>(
  arr: T[], 
  callbackfn: (value: T, index: number, array: T[]) => Promise<U>, 
  thisArg?: any
) 
  return await Promise.all(arr.map(async (value, index, array) => 
    try 
      return await callbackfn(value, index, array);
     catch(e) 
      throw e;
    
  , thisArg));

【讨论】:

【参考方案12】:

一种方法是Promise.all (ES6)。

此答案适用于 Node 4.0+。旧版本需要 Promise polyfill 或库。我还使用了 ES6 箭头函数,您可以将其替换为 Node functions。

此技术使用 Promise 手动包装 request.get。你也可以使用像request-promise 这样的库。

function urlToBase64(url) 
  return new Promise((resolve, reject) => 
    request.get(url, function (error, response, body) 
      if (!error && response.statusCode == 200) 
        resolve("data:" + response.headers["content-type"] + ";base64," + new Buffer(body).toString('base64'));
       else 
        reject(response);
      
    );
  )
 

// Map input data to an Array of Promises
let promises = input.map(element => 
  return urlToBase64(element.image)
    .then(base64 => 
      element.base64Data = base64;
      return element;
    )
);

// Wait for all Promises to complete
Promise.all(promises)
  .then(results => 
    // Handle results
  )
  .catch(e => 
    console.error(e);
  )

【讨论】:

非常通用的代码,如果你问我的话。他给出的代码会是什么样子? 另外我认为他想在服务器端使用 Node.js,因为他要求我希望他使用最新版本的承诺。我自己对此很感兴趣,我目前使用回调来映射具有异步请求的数组,存储请求的数量+完成的请求并仅在所有请求完成时才执行完成函数(这是所有请求的回调)。 Promises.all 看起来是一种更优雅的方式。 感谢 @joews 我将 NodeJS 更新到 v5,这段代码运行良好。 这是唯一适用于 map 内异步函数的语法【参考方案13】:

我在数组上使用异步函数。并且不使用array.map,而是使用for函数。是这样的:

const resultingProcessedArray = async function getSomeArray() 
    try 
      let  data  = await axios(url: '/myUrl', method:'GET'); //initial array
      let resultingProcessedArray = [];
      for (let i = 0, len = data.items.length; i < len; i++) 
        let results = await axios(url: `/users?filter=id eq $data.items[i].someId`, method:'GET');
        let domainName = results.data.items[0].domainName;
        resultingProcessedArray.push(Object.assign(data.items[i], domainName));
      
      return resultingProcessedArray;
     catch (err) 
      console.error("Unable to fetch the data", err);
      return [];
    
;

【讨论】:

以上是关于在地图中调用异步函数的最佳方法?的主要内容,如果未能解决你的问题,请参考以下文章

nodejs-在函数内执行多个异步调用的最佳方法?

React Hooks-调用依赖于“一次” useEffect的状态的函数的最佳位置?

在 iOS 10 中进行有效同步的异步调用的最佳方法

关闭异步 resteasy 客户端调用的最佳方法

如何在 ASP.NET 中等待异步 Web 服务调用的结果以获得最佳性能

Flutter:从 UI 调用异步代码的最佳实践