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 中的顺序对应排列的。

相关推荐
微学AI7 分钟前
详细介绍:MCP(大模型上下文协议)的架构与组件,以及MCP的开发实践
前端·人工智能·深度学习·架构·llm·mcp
liangshanbo121537 分钟前
CSS 包含块
前端·css
Mitchell_C38 分钟前
语义化 HTML (Semantic HTML)
前端·html
倒霉男孩40 分钟前
CSS文本属性
前端·css
shoa_top1 小时前
JavaScript 数组方法总结
javascript
晚风3081 小时前
前端
前端
JiangJiang1 小时前
🚀 Vue 人如何玩转 React 自定义 Hook?从 Mixins 到 Hook 的华丽转身
前端·react.js·面试
鱼樱前端1 小时前
让人头痛的原型和原型链知识
前端·javascript
用户19727304821961 小时前
传说中的开发增效神器-Trae,让我在开发智能旅拍小程序节省40%时间
前端
lianghj1 小时前
前端高手必备:深度解析高频场景解决方案与性能优化实战
前端·javascript·面试