You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 02:57:18