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

问题描述 投票:0回答: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 进行转译?

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

javascript node.js asynchronous
14个回答
173
投票

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(error) {
         return {...item, error } ;
      }
  }));
  /* we can filter errors in data and retry later 
   * eg:
   * const errorItems = data.filter(item => !!item.error)
   */
 

80
投票

一种方法是

Promise.all
(ES6)

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

 的常规 
function< 4.

此技术用 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);
  })

20
投票

在 2020 年,我们现在有了 ECMAScript2021

for await...of 语法,它显着简化了事情:

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

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

8
投票

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


5
投票

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

3
投票

尝试下面的异步地图函数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]

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

1
投票

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

1
投票

出于生产目的,您可能想要使用像 lodasync 这样的库,您不应该重新发明轮子:

import { mapAsync } from 'lodasync'

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

它使用 Promise,没有依赖项,并且速度尽可能快。


1
投票

通过使用 Promise.all,您可以使 mapforEach 使用异步函数(即 Promises)。

要使 filtersomeevery 工作,您可以首先使用异步映射(依次使用 Promise.all),然后遍历 true/false 值并同步执行过滤/评估。

要使 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);

1
投票

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

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

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
所以你不必担心。


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

0
投票

这是一个简单的功能,可以让您选择

await
每个映射操作(
serial
)或运行
parallel
中的所有映射。

映射器函数也不需要返回

promise

    async function asyncMap(items, mapper, options = {
      parallel: true
    }) {
      if (options.parallel) {
        const promises = items.map(it => mapper(it))
        return Promise.all(promises)
      } else {
        const ret = []
        for (const item of items) {
          ret.push(await mapper(item))
        }
        return ret
      }
    }
}

打字稿版本

    async function asyncMap<T, V>(items: T[], mapper: (item: T) => V, options = {
      parallel: true
    }): Promise<V[]> {
      if (options.parallel) {
        const promises:Promise<V>[] = items.map(it => mapper(it))
        return Promise.all(promises)
      } else {
        const ret:V[] = []
        for (const item of items) {
          ret.push(await mapper(item))
        }
        return ret
      }
    }

科学工作片段

async function asyncMap(items, mapper, options = {
  parallel: true
}) {
  if (options.parallel) {
    const promises = items.map(it => mapper(it))
    return Promise.all(promises)
  } else {
    const ret = []
    for (const item of items) {
      ret.push(await mapper(item))
    }
    return ret
  }
}

// A test to multiply number by 2 after 50 milliseconds
function delay(num) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('resolving after ', Date.now() - start, 'ms')
      resolve(num * 2)

    }, 50)
  })
}

const start = Date.now();
(async() => {
  const data = [1, 2, 3, 4]
  const resParallel = await asyncMap(data, it => delay(it))
  const resSerial = await asyncMap(data, it => delay(it), {
    parallel: false
  })
  console.log(data)
  console.log(resParallel)
  console.log(resSerial)

})();

© www.soinside.com 2019 - 2024. All rights reserved.