问一个循环执行的问题

在做一个微信小程序的多图片上传功能,采用canvas绘制功能对大图片进行绘制压缩保存。

因为是多选图片上传,所以使用了一个循环对绘制保存。

uploadfilelist.forEach(function(item){
that.canvasimg(item)
})
.....
canvasimg(image){
....
 ctx.drawImage(img, 0, 0, picWidth, picHeight); //先画出图片
            //延迟600ms,避免部分机型未绘制出图片内容就执行保存操作,导致最终的图片是块白色画板。
            setTimeout(() => {
              wx.canvasToTempFilePath({
                fileType: "jpg",
                canvas: canvas,
....

这里有个问题就保存图片时设置了一个600ms后执行保存动作(原因:避免部分机型未绘制出图片内容就执行保存操作,导致最终的图片是块白色画板。),但是用户上传来的图片列表uploadfilelist循环是立即执行的,导致第一张图片还没有保存就绘制了下一张图片,直接覆盖了上一张图片。
得到结果,全部是同一张图片!

阅读 1.2k
1 个回答

延迟使用 setTimeout,是一个异步操作。这个问题本质上是要解决“按顺序执行异步操作”的问题。

先说解决这个问题最简单的办法是用 Promise + async/await。然后下面再来分析,具体一点解决方案在本回答靠后的地方。

一般异步操作是通过回调来连接的,即异步结束触发回调,回调中开始另一个异步操作。回调要按顺序执行本身有点困难,因为组装回调确实 …… 不知道该怎么形容。不过可以采用一个变通的办法,就是“任务队列”。

任务队列是把要做的任务封装成函数放在一个队列中,每完成一个任务,就出列一个继续进行,直到没有任务或者超过一定时限。

先用 setTimeout 模拟一个画图程,需要消耗一定时间,但保证是在 600 毫秒内。

// 模拟画图,需要一定时间完成,保证在 600 毫秒内
function drawImage(value) {
    const elapsed = ~~(Math.random() * 400 + 100);
    setTimeout(
        () => console.log(`complete drawing ${value} in ${elapsed}ms`),
        elapsed
    );
}

然后使用队列的方法

// 为每个要绘制的内容封装一个函数,保存了 task 队列中。
// 注:JS 的数组兼有列表、栈、队列的功能
const tasks = [1, 2, 3, 4].map(value => () => drawImage(value));

// 定义一个执行函数,每次调用会从队列中取出一个函数执行,
// 等待 600 毫秒确保执行完成之后再调用 doNext() 进行下一次任务
function doNext() {
    const fn = tasks.shift();
    if (!fn) {
        // 没任务了,就结束
        console.log("all done.");
        return;
    }

    fn();
    setTimeout(
        () => doNext(),
        600
    );
}

// 启动
doNext();

然而,其实,这些事情 JS 引擎都可以帮我们做,使用 await 即可。但是在使用 await 之前需要先封装 Promise。由于 drawImage() 是模拟的绘图,所以我们不修改这个函数,只对它进行一个 600 毫秒的封装

async function drawImageInTime(value) {
    drawImage(value);
    return new Promise(resolve => {
        setTimeout(() => resolve(), 600);
    });
}

const images = [1, 2, 3, 4];
// 不能用 forEach(),需要用 for ... of
for (const v of images) {
    await drawImageInTime(v);
}

顺便一提,刚查了一下微信小程序的 Canvas API,案例中 drawImage() 之后会调用 draw()
,而 draw() 应该是实际进行绘制,有一个回调函数参数,在绘制完成时触发,这样的话就不需要固定等待 600 毫秒了。

封装 Promise 大概是这样

function waitDrawImage(v) {
    drawImage(v);
    return new Promise(resolve => draw(true, resolve));
}

没有去搭环境测试,所以这个是猜测的,自己验证一下。

文档里也没写得很清楚,但是我记得小程序改进过 API,异步操作在没有 callback 的时候都会返回 Promise,所以都有可能不需要自己封装 Promise。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题