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

Electron中jQuery操作在Promise.all完成前提前执行的问题

问题

在Electron和Node环境中编写了一段异步代码,在jQuery each循环中执行,预期所有异步操作完成后再执行后续逻辑:

$(async() => {
  if (await window.electronAPI.delete_items(project.name)) {
    var _count = 0;
    $.each(project.items, function(i, item) {
      var promises = item.subitems.map(async function(subitem) {
        return await window.electronAPI.update_item(subitem);
      });

      Promise.all(promises).then(function() {
        _count++;
        if (_count >= project.items.length) {
          $("#overlay").css("display", "none");
          console.log("Done");
        }
      });
    });
  }
});

整体逻辑运行正常,但$("#overlay").css("display", "none");会立即执行,而console.log("Done");能正确在所有item更新完成后运行。请问如何避免jQuery操作过早执行?

补充完整代码:

// index.js
app.whenReady().then( () => {

  ...

    ipcMain.handle( "delete_all_cached_images", delete_all_cached_images );
    ipcMain.handle( "update_cached_image", update_cached_image );

    /* Create the window */
    createWindow();

    app.on( "activate", () => {
        
        /* Fix MacOS multi window bug */
        if( BrowserWindow.getAllWindows().length === 0 ) createWindow();
    } );
} );

async function update_cached_image( e, project_name, image_type, image_size, image_name, image_data ) {

    /* Create a new blank image */
    let image = new Jimp( image_size, image_size, function ( err, image ) {
        
        if( err )
            return false;

        /* Map the pixels correct */
        var _image_data = Array.from( { length: image_size }, () => Array.from( { length: image_size }, () => undefined ) );

        for( var row_sel = 0; row_sel < image_size; row_sel++ )
            for( var col_sel = 0; col_sel < image_size; col_sel++ )
                _image_data[ col_sel ][ row_sel ]  = image_data.data[ row_sel ][ col_sel ];

        /* Loop through each pixel to add to the image */
        _image_data.forEach( ( row, y ) => {

            row.forEach( ( colour, x ) => {

                /* If we have a transparent pixel, don't add it to the image */
                if( ( colour == "" ) || ( colour == null ) )
                    image.setPixelColor( Jimp.rgbaToInt( 0, 0, 0, 0 ), parseInt( x ), parseInt( y ) );
                else 
                    image.setPixelColor( Jimp.cssColorToHex( "#" + colour ), parseInt( x ), parseInt( y ) );
            } );
        } );

        /* Resize to a nice large size */
        image.resize( 512, 512 , Jimp.RESIZE_NEAREST_NEIGHBOR );

        /* Save the image to project directory */
        image.write( path.join(  __dirname, "projects", project_name, "cache", image_type, image_name + ".png" ), ( err ) => {
        
            if( err )
                return false;
        } );
    } );

    return true;
}

// preload.js
const { contextBridge, ipcRenderer } = require( "electron" );

contextBridge.exposeInMainWorld( "electronAPI", {

    ...

    delete_all_cached_images: ( project_name ) => ipcRenderer.invoke( "delete_all_cached_images", project_name ),
    update_cached_image: ( project_name, image_type, image_size, image_name, image_data ) => ipcRenderer.invoke( "update_cached_image", project_name, image_type, image_size, image_name, image_data )
} );

// renderer.js
function update_cached_images() {

    $( async () => {

        /* First delete all old cached images */
        if( await window.electronAPI.delete_all_cached_images( project.name ) ) {


            var ti = 0;
            Promise.all( project.textures.map( g_texture => {
                
                return Promise.all( g_texture.textures.map( texture => {

                    return window.electronAPI.update_cached_image( project.name, "textures", 8, ( g_texture.name + "_" + ti++ ), texture );
                } ) );
            } ) ).then( () => {
                
                $("#overlay").css("display", "none");
                console.log("Done");
            } ).catch(err => {

                console.log(err);
                // add something to handle/process errors here
            } );
            
        } else {

            show_error( "Error caching project textures." );
        }
    } );
}

原因分析

  1. 计数逻辑的不可靠性:
    原始代码用_count变量统计完成的异步任务,但$.each是同步循环,会瞬间启动所有Promise.all任务。如果某组子任务意外快速完成(比如空数组、无实际异步操作),会导致_count提前达到阈值,触发overlay隐藏操作。

  2. 主进程异步逻辑的漏洞:
    update_cached_image标记为async,但内部Jimp的创建、写入都是回调式异步操作,函数直接返回true,没有等待这些异步操作完成。这会让前端的ipcRenderer.invoke提前收到返回值,误以为任务已完成。


解决方案

1. 重构前端异步逻辑,用Promise.all统一管理

放弃计数方式,直接收集所有层级的Promise,等待全部完成后再执行后续操作:

$(async() => {
  if (await window.electronAPI.delete_items(project.name)) {
    // 收集所有子任务的Promise
    const allPromises = project.items.map(item => {
      return Promise.all(item.subitems.map(subitem => {
        return window.electronAPI.update_item(subitem);
      }));
    });

    // 等待所有任务完成
    await Promise.all(allPromises);
    $("#overlay").css("display", "none");
    console.log("Done");
  }
});

2. 修复主进程update_cached_image的异步逻辑

将Jimp的回调API包装为Promise,确保函数真正等待图片处理和写入完成:

async function update_cached_image(e, project_name, image_type, image_size, image_name, image_data) {
  try {
    // 包装Jimp创建操作
    const image = await new Promise((resolve, reject) => {
      new Jimp(image_size, image_size, (err, img) => {
        if (err) reject(err);
        else resolve(img);
      });
    });

    // 像素映射逻辑不变
    const _image_data = Array.from({ length: image_size }, () => Array.from({ length: image_size }, () => undefined));
    for (let row_sel = 0; row_sel < image_size; row_sel++) {
      for (let col_sel = 0; col_sel < image_size; col_sel++) {
        _image_data[col_sel][row_sel] = image_data.data[row_sel][col_sel];
      }
    }

    _image_data.forEach((row, y) => {
      row.forEach((colour, x) => {
        if (!colour) {
          image.setPixelColor(Jimp.rgbaToInt(0, 0, 0, 0), parseInt(x), parseInt(y));
        } else {
          image.setPixelColor(Jimp.cssColorToHex("#" + colour), parseInt(x), parseInt(y));
        }
      });
    });

    image.resize(512, 512, Jimp.RESIZE_NEAREST_NEIGHBOR);

    // 包装图片写入操作
    await new Promise((resolve, reject) => {
      const savePath = path.join(__dirname, "projects", project_name, "cache", image_type, image_name + ".png");
      image.write(savePath, (err) => {
        if (err) reject(err);
        else resolve();
      });
    });

    return true;
  } catch (err) {
    console.error(err);
    return false;
  }
}

3. 优化renderer.js逻辑(针对补充代码)

保持Promise.all层级结构,同时用await替代then回调,让逻辑更清晰:

function update_cached_images() {
  $(async () => {
    if (await window.electronAPI.delete_all_cached_images(project.name)) {
      let ti = 0;
      try {
        await Promise.all(project.textures.map(g_texture => {
          return Promise.all(g_texture.textures.map(texture => {
            return window.electronAPI.update_cached_image(
              project.name, 
              "textures", 
              8, 
              `${g_texture.name}_${ti++}`, 
              texture
            );
          }));
        }));
        $("#overlay").css("display", "none");
        console.log("Done");
      } catch (err) {
        console.log(err);
        show_error("Error caching project textures.");
      }
    } else {
      show_error("Error caching project textures.");
    }
  });
}

关键要点

  • 避免用计数变量跟踪异步任务,Promise.all是更可靠的批量异步管理方式。
  • 主进程中所有回调式异步操作必须包装为Promise,确保IPC调用的返回值真正对应任务完成状态。
  • 优先使用await替代then回调,简化异步代码的时序逻辑。

内容的提问来源于stack exchange,提问作者GSephElec

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 12:10:55