JavaScript中如何等待所有.toBlob异步操作完成后再执行后续逻辑?
解决Canvas.toBlob异步批量处理的优雅方案
你遇到的核心问题就是没有正确管理toBlob的异步生命周期——之前尝试的Promise数组失效,是因为你的Promise没有真正等待createImages内部的4个toBlob操作完成就直接resolve了,导致回调提前触发。而你最后用的计数器方案虽然能工作,但会重复触发saveit()(四个toBlob回调都可能触发判断),而且代码冗余。
我们可以通过把toBlob封装成Promise,结合async/await和Promise.all来彻底解决这个问题,让代码更清晰、更可靠:
第一步:封装toBlob为Promise
先写一个通用的工具函数,把原生的回调式toBlob转换成支持then/await的Promise:
// 封装Canvas.toBlob为Promise function canvasToBlob(canvas, type = 'image/jpeg', quality = 0.9) { return new Promise((resolve, reject) => { canvas.toBlob(blob => { blob ? resolve(blob) : reject(new Error('Failed to generate blob from canvas')); }, type, quality); }); } // 封装元素(比如你的loadedData,看起来是canvas)的toBlob为Promise function elementToBlob(element, type = 'image/jpeg', quality = 0.9) { return new Promise((resolve, reject) => { element.toBlob(blob => { blob ? resolve(blob) : reject(new Error('Failed to generate blob from element')); }, type, quality); }); }
第二步:改造createImages为Async函数
让createImages内部等待所有4个toBlob操作完成,确保每个图片的4种规格都生成后才标记完成:
async function createImages(loadedData, imgNum, filename) { const mime_type = 'image/jpeg'; let cvs = document.createElement('canvas'); // 1. 生成detail规格图片 cvs.width = 800; cvs.height = 800; const ctx = cvs.getContext('2d'); ctx.drawImage(loadedData, 0, 0, 800, 800); // 加水印逻辑 if (imgNum === 1 && sgiArr.indexOf(filename.split('-')[2]) >= 0) { const watermark = document.getElementById(`sgi${filename.split('-')[2]}`); ctx.drawImage(watermark, 10, 720); // 更新显示图片 $(`img[filename="${filename}"]`).attr('src', cvs.toDataURL(mime_type)); } // 等待detail blob生成并添加到FormData const detailBlob = await canvasToBlob(cvs, mime_type, 0.96); kpData.append('detail', new File([detailBlob], filename, { type: mime_type })); // 2. 生成general规格图片 cvs.width = 370; cvs.height = 370; cvs.getContext('2d').drawImage(loadedData, 0, 0, 370, 370); const generalBlob = await canvasToBlob(cvs, mime_type, 0.96); kpData.append('general', new File([generalBlob], filename, { type: mime_type })); // 3. 生成thumbnail规格图片 cvs.width = 240; cvs.height = 240; cvs.getContext('2d').drawImage(loadedData, 0, 0, 240, 240); const thumbBlob = await canvasToBlob(cvs, mime_type, 0.96); kpData.append('thumb', new File([thumbBlob], filename, { type: mime_type })); // 4. 生成repository规格图片 const repoBlob = await elementToBlob(loadedData, mime_type, 0.92); repositoryData.append('imgfiles', new File([repoBlob], filename, { type: mime_type })); }
第三步:改造processSku和点击事件逻辑
让processSku返回Promise,确保每个SKU的图片处理完成后才被计入Promise数组,最后用Promise.all等待所有操作完成:
$(document).on('click', '.saveSkuImages', async function() { // 初始化变量 kpData = new FormData(); repositoryData = new FormData(); skuString = ','; const skuContainer = $(this).closest('.skuContainer'); const sku = skuContainer.attr('sku'); const skuImgContainers = skuContainer.find('.skuImgContainer'); const promises = []; // 遍历每个图片容器,收集处理Promise skuImgContainers.each(function() { const sic = $(this); promises.push(processSku(sic, sku)); }); // 等待所有SKU的所有图片处理完成 try { await Promise.all(promises); console.log('所有图片规格已生成,触发保存'); saveit(); } catch (err) { console.error('图片处理失败:', err); // 这里可以添加错误处理逻辑,比如提示用户 } }); // 让processSku返回Promise,等待createImages完成 async function processSku(sic, sku) { const skuImg = sic.find('.skuImg'); const imgContainer = skuImg.find('.imgContainer'); const img = imgContainer.find('img:first'); const cvs = $(`#${img.attr('forcanvas')}`); const imgNum = parseInt(skuImg.attr('imgnum')); const filename = `${sku}${imgNum > 1 ? `-alt${imgNum}` : ''}.jpg`; img.attr('filename', filename); // 标记用于季节性的图片 if (sic.find('.useForSeasonal').is(':checked')) { kpData.append('parentImage', filename); } // 如果有canvas才处理,否则直接跳过(已存在的图片) if (cvs.length) { await createImages(cvs[0], imgNum, filename); } }
为什么这个方案更可靠?
- 彻底管理异步生命周期:每个
toBlob操作都被封装成Promise,await确保前一个操作完成后再进行下一个,不会出现异步操作乱序的问题。 - 避免重复触发saveit:
Promise.all只会在所有异步操作完成后执行一次saveit(),不会像计数器方案那样多次调用。 - 代码更清晰:去掉了冗余的计数器判断,逻辑更直观,也更容易维护和调试。
另外,如果你想保留之前的计数器方案(比如需要实时显示进度),可以在每个await之后递增计数器,但记得要加一个锁避免重复调用saveit():
let isSaving = false; function checkAndSave() { if (numFilesCreated === numFiles && !isSaving) { isSaving = true; saveit(); } }
然后在每个toBlob的Promise回调里调用checkAndSave()即可。
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

