map + Promise.all 实现按顺序获取并发请求的结果

前言

之前在《canvas + ts 实现将图片一分为二的功能,并打包发布至 npm》中提到公司有个需求是将用户准备上传的图片一分为二后再真正地上传到服务器。由此还有个相关的需求:当后端处理好图片后,会把左右两个半张图片的地址按先左后右的顺序传回前端,我需要把它们生成图片后拼接成一整张展示。根据地址生成图片的代码大致如下:

javascript 复制代码
const imgs = []
const img = new Image()
img.onload = () => {
  imgs.push(img)
}
img.src = url

这里有个问题 ------ 左右两张图片哪张先加载完毕不确定,即 img.onload 触发顺序不一定,那么 imgs 数组内的图片就不一定是按左右顺序排列的,拼接时如果按序从imgs 里取出图片绘制就不一定对。本文就来说说我是如何解决这一问题的。

使用 for 循环 + promise + async/await

我首先想到的是使用 for 循环 + promise + async/await 来按序加载图片,代码如下,为了方便演示,图片加载完毕后传入 resolve() 的值为图片的 src

javascript 复制代码
// 代码片段 1
const arr = ['./imgs/left.jpg', './imgs/right.jpg']
const imgs = []

for (let index = 0; index < arr.length; index++) {
  const res = await getImg(arr[index])
  imgs.push(res)
}
console.log(imgs)

function getImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = () => {
      resolve(img.src)
    }
    img.src = url
  })
}

这确实能够让 imgs 内的图片是按左右顺序排列的,但缺点是需等左边图片请求加载完毕后,右边的图片才能开始请求加载。如果图片较大或网络环境较差,就会比较费时:

改用 map + Promise.all

相比于定义 for 循环,我更喜欢直接使用数组的 forEach 方法。但如果把代码片段 1 中的 for 循环替换成如下代码:

javascript 复制代码
// 代码片段 2
arr.forEach(async item => {
  const res = await getImg(item)
  imgs.push(res)
})

会发现打印得到的 imgs 为空数组,并且两张图片是并发请求的:

这说明在 forEach 中使用 async/await 是达不到预期效果的。第 2 次的 getImg(item) 的调用并没有等待第 1 次的调用有结果后再执行,这其实可以解决使用 for 循环耗时的问题,但问题是我们得不到正确的 imgs。如果把 forEach 改为 map 方法,对图片的请求也是并发进行的,但不同于 forEach 的是,map 方法会创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。我们可以利用这一点将代码片段 2 改造成如下所示:

javascript 复制代码
// 代码片段 3
const promiseList = arr.map(async item => {
  const res = await getImg(item)
  return res
})

promiseList 数组里的值为 promise 对象,如果等图片加载后去查看 ,结果如下:

请注意,因为我们让 map 的回调变为了 async 函数,而执行 async 函数得到的返回值一定是个 promise 对象。所以虽然代码片段 3 中看起来返回的是 respromiseList 里的元素似乎应该是 img.src,但其实相当于是返回了 Promise.resolve(res)

promiseList 里的 promise 对象的 PromiseState 均为 fulfilled,并且 PromiseResult 的值与 arr 里的顺序对应,是按照左在前,右在后的顺序排列的。我们可以将 promiseList 传入 Promise.all(),通过 .then() 以获取这些 PromiseResult。获取到的 res 也是一个数组,其中元素的顺序是按照传入 Promise.all() 的数组的顺序,而不是各个 promise 执行完成的顺序,故而也是左在前,右在后 。

完整代码与效果

完整代码如下:

javascript 复制代码
// 模拟后端返回的数据
const arr = ['./imgs/left.jpg', './imgs/right.jpg']
// 保存图片
let imgs = []
// 获取图片
const promiseList = arr.map(async item => {
  const res = await getImg(item)
  return res
})

// 获取图片请求结果
Promise.all(promiseList).then(res => {
  imgs = res
  console.log(imgs)
})

// 获取图片方法
function getImg(url) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = () => {
      resolve(img.src)
    }
    img.src = url
  })
}

运行效果如下:

可以看到,浏览器对图片的请求是并行发起的,而最终得到的 imgs 中的元素,是按照 arr 中的顺序对应排列的。

相关推荐
IT_陈寒15 分钟前
JavaScript性能优化:这7个V8引擎技巧让我的应用速度提升了50%
前端·人工智能·后端
学渣y28 分钟前
nvm下载node版本,npm -v查看版本报错
前端·npm·node.js
excel34 分钟前
首屏加载优化总结
前端
敲代码的嘎仔40 分钟前
JavaWeb零基础学习Day1——HTML&CSS
java·开发语言·前端·css·学习·html·学习方法
Tachyon.xue3 小时前
Vue 3 项目集成 Element Plus + Tailwind CSS 详细教程
前端·css·vue.js
细节控菜鸡3 小时前
【2025最新】ArcGIS for JS二维底图与三维地图的切换
javascript·arcgis
FuckPatience4 小时前
Vue 中‘$‘符号含义
前端·javascript·vue.js
东风西巷6 小时前
K-Lite Mega/FULL Codec Pack(视频解码器)
前端·电脑·音视频·软件需求
超级大只老咪7 小时前
何为“类”?(Java基础语法)
java·开发语言·前端
你的人类朋友9 小时前
快速搭建redis环境并使用redis客户端进行连接测试
前端·redis·后端