typescript异步方法-JavaScript 循环中调用异步函数的三种方法

JavaScript循环调用异步函数的三种方式,以及forEach不起作用的原因分析

本文主要分析如何在循环体中调用异步函数,满足循环中调用异步函数并在异步函数的值返回后处理后续业务的同步要求。

这篇文章的灵感来自于和柳青在群里的一次讨论。 讨论的主要问题只是循环体内的异步调用。 他还写了自己的总结:节点['解决方案']_中异步循环的问题_从异步风暴的支持开始,用async和await for map循环和for循环。

商业分析

据我了解,当时讨论的问题是基于这样一个需求:

首先需要调用API来获取数据

获取到的数据是一个链表类型,这里叫arr。

会遍历arr,遍历过程中会调用其他API获取数据,但是会对数据进行一些操作。

整体业务逻辑和需求,模拟如下:

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
  [4, 'four'],
  [5, 'five'],
]);
// 用 setTimeout 模拟异步的 api 调用
// timeout 会获得一个数组类型的数据,随后会有另外的 api 根据数组内的数据,再一次去进行异步调用,获取其他数据
const timeout = () =>
  new Promise((resolve) => setTimeout(() => resolve([1, 2, 3, 4, 5])), 1000);
// 循环体内调用的数据
const getEl = (key) =>
  new Promise((resolve) => setTimeout(() => resolve(map.get(key)), 1000));
const getData = () => {
  const data = timeout();
  let str = [];
  // 这里没有处理异步操作,所以会有语法错误
  data.forEach((el) => {
    const elVal = getEl(el);
    str.push(elVal);
  });
  // 最后输出结果应该是 ['one', 'two', ...] 这样一个包含 异步调用后返回值 的数组
  console.log(str);
};
getData();

其实只是一个基本的逻辑实现,并没有实现异步操作。 现在直接运行的话会报错。 但基本逻辑在这里:

API从哪里获取数值数据? 遍历数据。 遍历过程中,继续调用API获取值并进行操作。第一版的问题

虽然最初的计划比较简单,但使用 async/await 来获取数据:

// 其他函数没有改动,只修改了 getData 这一部分
const getData = async () => {
  // 使用 await 语法糖
  const data = await timeout();
  let str = [];
  // 加上 async 和 await 去等待异步调用
  data.forEach(async (el) => {
    const elVal = await getEl(el);
    str.push(elVal);
    // 可以正常输出
    console.log(elVal);
  });
  // 返回值却是一个空数组
  console.log(str);
};
getData();

输出结果并不令人满意。 命令行的输出顺序如下:

[]
one
two
three
four
five

可以看到,异步数据获取发生在输出字段之后typescript异步方法,这也意味着forEach内部异步调用的顺序与预期不符。

解决方案

改为for循环体是柳青在总结中提出的解决方案; 这里还有两个不使用 for 循环体的解决方案。

传统的 for 循环

一种解决方案是将 forEach/map 替换为传统的 for(leti=0;i <arr.lengt;i++)这样的传统写法,如:

const getData2 = async () => {
  const data = await timeout();
  let str = [];
  for (let i = 0; i < data.length; i++) {
    const element = await getEl(data[i]);
    console.log(element);
    str.push(element);
  }
  console.log(str);
};
getData2();

最终输出是:

one
two
three
four
five
[ 'one', 'two', 'three', 'four', 'five' ]

该字段的输出结果在API调用结果之后,这意味着数据可以正常渲染或处理。

不使用for循环的解决方案

那么异步代码只能使用传统的for循环吗?

不一定,只是基于forEach的方案很难实现。

剖析为什么 forEach 不起作用

我在输出过程中发现了一些细微的异常。 例如,使用for循环时,每一行的输出都有一定的间隔。 毕竟await应该“锁定”操作,只有接收到数据后才能继续。 下次通话。 而使用 forEach 函数时,它会等待大约几秒钟,然后一次将所有结果一起输出。

直接用文字描述可能不太直观,所以就简单的几个时间戳。 一个是在刚才进入函数时复制当前时间,另一个是在循环体中输出值时复制当前时间,以便更直观的比较:

对于每个对于

也就是说,forEach的循环调用没有await上面的异步操作。 因此,forEach中的同步代码执行完后,异步代码就开始执行,这就是为什么forEach的代码在控制台复制异步调用中获得的值之前会输出一个空字段。

异步调用的准备资料在这里:【万字解读】JavaScript中的异步模式以及Promise的使用

这样,函数顶部就已经声明了async关键字,并且在forEach中也使用了await来等待数字。 而且,显然awaittimeout() 有效,为什么只有forEach 不起作用?

那是因为整个forEach函数并没有使用await来等待,整个forEach是同步执行的。 forEach的实现是基于内部反弹函数的执行。 因此,进入循环时typescript异步方法,函数内部会调用传入的反弹函数。 当bounce函数是异步的时候,bounce函数会被加载到时间循环机制中,forEach会继续执行里面的同步代码,即继续循环。

遗憾的是,基于历史原因——forEach函数是ES5时代的函数,ES6之后才支持Promise等异步操作——无法使用forEach直接在循环体中调用异步函数。

然而,现在已经是 2021 年了,这并不意味着没有解决方案。

并行解决方案

如果数据之间没有依赖关系,虽然我个人推荐使用这些方法,但相对效率会更高。

实现方法是Promise.all结合await和map来实现:

因此,结合Promise.all、await和map,可以近似同步地发送多个异步请求。 之所以说类似,是因为它实际上是一个迭代,需要按照链表第一个元素的顺序执行。 然而,在大多数情况下,与异步操作相比,链表的迭代可以花费尽可能少的时间。

实现如下:

const getData = async () => {
  const data = await timeout();
  const curr = new Date();
  console.log(curr);
  let str = [];
  // 使用 Promise.all 去等待内部所有的 Promise 执行完毕
  await Promise.all(data.map((el) => getEl(el))).then((val) => {
    str = val;
    console.log(new Date() - curr);
    return val;
  });
  console.log(str);
};
getData();

效果截图:

可以看到,相比最初使用传统的for循环,使用Promise.all可以有效提升性能。 当存在多个持续时间较长的异步任务且它们之间没有依赖关系时,为了提高用户体验,最好使用Promise.all来调用。

这是因为await会等待所有Promise执行的结果,即Promise.all被锁定,而内部map仍然同步执行。 所以对于循环体中的异步函数来说,不需要等待上一次迭代完成才执行下一次迭代——await 的语句糖会等待 Promise 执行完之后再执行下一次 Promise。

其执行流程大致如下:

串行解决方案

forawait...of是基于iterable(可迭代)的实现。 这些实现更适合具有依赖性的内容。 例如,较大文件的加载可以在读取到某一点时触发文件下一段的加载,从而达到提高用户体验的效果。

用例如下:

const getData3 = async () => {
  const data = await timeout();
  const curr = new Date();
  console.log(curr);
  let str = [];
  for await (el of data) {
    const element = await getEl(el);
    console.log(element, new Date() - curr);
    str.push(element);
  }
  console.log(str);
};
getData3();

疗效如下:

由于await用于等待上一个异步调用的结果返回后再执行下一个异步调用,所以会消耗较多的时间。

其执行流程大致如下:

总结

总的来说,在循环体中调用异步函数有以下三种方式: