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

时间:2015-10-30 14:18:21

标签: javascript node.js

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

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进行转换?

目前实施此方法的最佳方式是什么?

谢谢!

13 个答案:

答案 0 :(得分:31)

一种方法是Promise.all (ES6)

此答案适用于Node 4.0+。旧版本需要Promise polyfill或库。我还使用了ES6箭头函数,你可以用常规catch代替Node< 4。

此技术使用Promise手动包装function。您还可以使用request-promise等库。

request.get

答案 1 :(得分:25)

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');
    }
  });
}

编辑@ 2018/04/29:我为每个人提供了一般的例子:

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

      return item;
  });

答案 2 :(得分:5)

您可以使用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

答案 3 :(得分:2)

2019

使用Promise.all警告

,它们没有按照确切的顺序执行,有些服务器无法支持同时执行查询或api调用。

更多新库使我的单页应用程序构建了超过4个megas。所以我决定不添加像lodash等更多的新库,这些库可以替代代码。我使用等待地图的好方法是:

Coalesce(Your_Column, 0)

答案 4 :(得分:1)

我在数组上使用异步函数。而不是使用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 [];
    }
};

答案 5 :(得分:1)

通过使用 Promise.all ,您可以使地图 forEach 使用异步功能(即Promises)。

要使过滤器一些每个工作,您可以先使用异步映射(依次使用Promise.all),然后再使用遍历true / false值并同步进行过滤/评估。

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

使用此知识,可以以某种方式修改原始数组方法,以使它们继续“正常”地使用正常/同步功能,但也可以使用异步功能。

// 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);

答案 6 :(得分:0)

为了方便起见,我不得不写这个。否则,我可能需要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));
}

答案 7 :(得分:0)

出于生产目的,您可能想使用lodasync之类的库,因此您不应该重新发明轮子:

import { mapAsync } from 'lodasync'

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

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

答案 8 :(得分:0)

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 :(得分:0)

我遇到了类似的问题,发现这比较容易(我使用的是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)));

答案 10 :(得分:0)

在2020年,我们现在拥有for await...of syntaxECMAScript2021,大大简化了事情:

因此您现在可以简单地执行以下操作:

//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){
   ....
}

答案 11 :(得分:0)

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

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;
}

答案 12 :(得分:0)

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

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

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

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

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

示例

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  
})()))