fabric.js项目中如何让JS事件循环等待异步函数执行完成
问题描述
当前基于fabric.js开发项目,需要实现JS执行流等待前置函数完成后再执行后续逻辑,业务流程包含三个步骤:
- 从数据库读取图片地址
- 替换画布上的占位图片
- 通过ajax将画布保存为PNG格式
实际运行时添加日志打印执行顺序,发现主循环执行完毕后,图片替换逻辑的日志才输出,ajax保存操作的完成时间更晚,不符合预期。
问题复现代码(伪代码)
for (/* 外层遍历逻辑 */) { console.log("start"); // 遍历从数据库拿到的图片地址数组 for (/* 内层遍历逻辑 */) { console.log("Inside Second loop"); // 遍历画布上需要替换的占位图/图层,单画布可能存在多个待替换图片 // 此处执行图片替换逻辑 console.log("BeforeImageChange"); imgToChange.setSrc(imgAddressFromDB, () => { console.log("AfterImageChange"); }) } function saveCanvas(){ // 通过ajax提交到php接口,将画布保存为PNG图片的逻辑 console.log("AfterSaving"); } saveCanvas(); console.log("End"); }
实际日志输出顺序
"start" "Inside Second loop" "BeforeImageChange" "End" "AfterImageChange" "AfterSaving"
预期执行顺序
AfterImageChange日志在BeforeImageChange之后立刻执行saveCanvas保存操作在循环结束前完成,即所有图片替换完成后再触发保存
问题原因
这个问题是JS异步执行机制的典型表现:fabric.js提供的setSrc方法本身是异步操作——内部需要发起资源请求加载图片,等图片加载完成后才会执行传入的回调函数;普通的ajax请求默认也是异步的,这两类操作都不会阻塞JS主循环的同步代码执行。
所以代码运行时会先把所有同步逻辑跑完(依次打印start、Inside Second loop、BeforeImageChange、End),等同步任务全部执行完,才会去任务队列里取出已经完成的异步任务回调执行,也就是后打印AfterImageChange、AfterSaving,和观察到的输出顺序完全一致。之前了解的异步处理方法完全适配这个场景,用async/await把异步流程改成按顺序等待即可。
解决方法
首先把setSrc和saveCanvas的逻辑都包装成Promise,然后在外层循环、内层循环都用await等待异步操作完成,改造后的代码示例:
// 包装setSrc为Promise版本 function setImageSrc(imgInstance, src) { return new Promise((resolve, reject) => { imgInstance.setSrc(src, () => { // 图片加载完成后如果画布没自动刷新,放开下面这行 // canvas.renderAll(); console.log("AfterImageChange"); resolve(); }, (err) => { reject(err); }) }) } // 包装saveCanvas为Promise版本 function saveCanvas() { return new Promise((resolve, reject) => { // 替换为实际的ajax提交逻辑,以下为原生ajax示例 const canvasData = canvas.toDataURL('image/png'); const xhr = new XMLHttpRequest(); xhr.open('POST', '对应的php保存接口地址'); xhr.onload = () => { if (xhr.status === 200) { console.log("AfterSaving"); resolve(xhr.response); } else { reject(xhr.statusText); } }; xhr.onerror = reject; xhr.send(canvasData); }) } // 外层执行函数加async关键字 async function processCanvas() { for (/* 外层遍历条件 */) { console.log("start"); // 遍历数据库返回的图片地址数组 for (/* 内层遍历条件,拿到待替换的imgToChange和对应imgAddressFromDB */) { console.log("Inside Second loop"); console.log("BeforeImageChange"); // 等待单张图片替换完成再进入下一次循环 await setImageSrc(imgToChange, imgAddressFromDB); } // 所有图片替换完成后,再等待保存操作完成 await saveCanvas(); console.log("End"); } } // 启动流程,加错误捕获 processCanvas().catch(err => { console.error('流程执行出错', err); });
优化&注意事项
- 不要为了省事把ajax改成同步模式(配置
async: false),会阻塞整个页面渲染,用户体验极差,用Promise+async/await是目前的标准解法 - 如果内层循环里有多张图片需要替换,不需要一张张串行等待的话,可以用
Promise.all批量等待所有图片加载完成,执行速度会快很多:const replaceTasks = []; for (/* 内层循环 */) { console.log("Inside Second loop"); console.log("BeforeImageChange"); replaceTasks.push(setImageSrc(imgToChange, imgAddressFromDB)); } // 等待所有图片并行替换完成 await Promise.all(replaceTasks);
内容的提问来源于stack exchange,提问作者Mandeep Singh
相关产品推荐
相关产品推荐

