前言
之前在《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 中看起来返回的是 res
,promiseList
里的元素似乎应该是 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
中的顺序对应排列的。