1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > JavaScript 循环中调用异步函数的三种方法 及为什么 forEach 无法工作的分析

JavaScript 循环中调用异步函数的三种方法 及为什么 forEach 无法工作的分析

时间:2022-01-29 07:11:49

相关推荐

JavaScript 循环中调用异步函数的三种方法 及为什么 forEach 无法工作的分析

JavaScript 循环中调用异步函数的三种方法,及为什么 forEach 无法工作的分析

业务分析初版的问题解决方案传统的 for 循环不使用 for 循环的解决方案分析 forEach 为什么不工作并行解决方案串行解决方案总结

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

这篇文章是受到和 六卿 在群里讨论问题时启发而写的,主要讨论的问题就只在循环体内进行异步调用。他也写了自己的总结: node 中循环异步的问题[‘解决方案‘]_源于 map 循环和 for 循环对异步事件配合 async、await 的支持。

业务分析

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

首先需要调用一个 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 那里获取到值data遍历data,在遍历中继续调用 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();

输出结果却不尽如人意,在命令行中输出的顺序是这样的:

[]onetwothreefourfive

可以看到,异步的数据获取是在输出数组之后发生的,这也代表 forEach 内的异步调用的顺序,不如预期所想。

解决方案

改为for循环体 是 六卿 在自己的总结内提出的解决方案;这里再提出了两个不使用for循环体 的解决方案。

传统的 for 循环

一个解决方案就是将forEach/map替换成传统的for (let i = 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();

最终的输出结果为:

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

数组的输出结果在 API 调用结果之后,也就意味着数据可以正常地被渲染或是处理。

不使用 for 循环的解决方案

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

也不尽然,只是解决方法无法基于forEach去实现而已。

分析 forEach 为什么不工作

在输出的时候我发现了一些微妙的异常,例如说使用 for 循环时,每一行的输出都是有一定间隔事件的——毕竟 await 应该会“锁”住运行,一直到数据接收之后才会进行下一步的调用。但是使用 forEach 函数时,它等待了大约几秒钟的时间,随后一下子将所有的结果一起输出。

直接用文字描述可能没有这么直观,那么就打几个时间戳。一个在刚刚进入函数的时候打印出当前时间,一个在循环体内输出值的时候打印出当前时间,更加直观的对比一下:

也就是说,forEach 的循环调用并没有 await 里面的异步操作。所以,当 forEach 中的同步代码执行完毕之后,异步代码才开始执行,这也是为什么 forEach 的代码先输出了一个空的数组之后,才在控制台上打印异步调用中获取的值。

异步调用的复习资料在这里:[万字详解]JavaScript 中的异步模式及 Promise 使用

那么,函数最上方已经声明了 async 关键字,forEach 中也使用了 await 去等数。而且,明明await timeout()工作了,为什么就只有 forEach 没有工作?

那是因为,forEach 整个函数没有使用 await 进行等待,整个 forEach 是同步执行的。forEach 的实现是基于内部的回调函数执行,因此,当进入循环之后,函数内部会去调用传进来的回调函数。当回调函数是异步时,回调函数就会被放入时间循环机制中,forEach 内部会继续去执行同步代码,也就是继续循环。

很可惜的是,基于历史原因——forEach 函数是 ES5 时代的函数,Promise 等异步操作的支持是 ES6 以后才有的支持——直接使用 forEach 是没有办法实现在循环体内调用异步函数的方法。

但是,都 年了,这也不代表没有解决方案。

并行解决方案

如果数据彼此之间没有依赖关系,其实个人更建议使用这种方式,相对而言效率会更高一些。

实现的方式是Promise.all结合awaitmap去实现:

Promise.all可以接收由 Promise 组成的数组,并且返回一个 Promise。

map的特性与 forEach 相似,区别在于前者会返回一个数组,后者会返回一个 undefined。

Promise.all的参数正好又是一个由 Promise 组成的数组;并且,Promise.all的返回值就是一个 Promise

await是 ES7 推出的语法糖,可以用来等待一个 Promise 的执行完成。

所以结合Promise.allawaitmap就可以近似同步地发送多个异步请求。之所以说是近似,还是因为毕竟是一个迭代,总归需要按序数组中第一个元素开始执行,只不过大多数情况下,数组的迭代与异步操作比起来消耗时间可以小到近乎不计。

实现如下:

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。

其执行流程大概如下:

串行解决方案

for await...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去等待上一个异步调用结果返回之后,再去执行下一个异步调用,因此消耗的时间也更多。

其执行流程大概如下:

总结

整体来说,在循环体内调用异步函数有以下三种方法:

传统for循环

最传统的解决方案

Promise.all,awaitmap的结合

可以近似同步地并行调用循环体内的 API,如果需要在数组之中循环调用 API,并且 API 之间彼此没有什么关联,那么使用这个方案可以极大的提升用户体验感

for await...of

for of的异步支持版本,可以串行调用 API,在不使用其他关键字的情况下与传统的for循环 效果一样

但是,因为for of是基于迭代器实现的,这也就代表着可以通过重写迭代器去实现一些特殊的业务场景,如:

视频渐进式下载

页面内容分段加载

这种效果在小说网站中用的还挺多的,为了防盗,部分小说网站在 VIP 章节中都是用图片代替文字。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。