promise.all队列异步请求取消某个上传

5 阅读4分钟

一、Promise.all 的核心特性与限制

Promise.all 用于并行执行多个 Promise,并在所有任务完成后返回结果数组。关键限制

  • 一旦调用,所有 Promise 都会开始执行,无法直接取消
  • 若其中一个 Promise 拒绝(reject),整个 Promise.all 会立即拒绝,无法继续执行其他任务。

二、取消异步请求的三种实现方案

1. 使用 AbortController(现代浏览器方案)

利用浏览器原生的 AbortController API 取消 fetch 请求或自定义异步任务:

function uploadFile(file, signal) {
  return new Promise((resolve, reject) => {
    // 模拟文件上传
    const xhr = new XMLHttpRequest();
    xhr.open('POST', '/api/upload');
    xhr.onload = () => resolve(xhr.response);
    xhr.onerror = () => reject(new Error('上传失败'));
    
    // 绑定取消信号
    if (signal) {
      signal.addEventListener('abort', () => {
        xhr.abort();
        reject(new Error('上传已取消'));
      });
    }
    
    xhr.send(file);
  });
}

// 并行上传多个文件
async function batchUpload(files) {
  const controllers = files.map(() => new AbortController());
  const uploadPromises = files.map((file, index) => 
    uploadFile(file, controllers[index].signal)
  );
  
  try {
    const results = await Promise.all(uploadPromises);
    return { success: true, results };
  } catch (error) {
    // 处理取消或错误
    if (error.message.includes('已取消')) {
      // 取消其他上传
      controllers.forEach(ctrl => ctrl.abort());
      return { success: false, error: '上传被取消' };
    }
    return { success: false, error: error.message };
  }
}

// 取消第2个上传
const files = [file1, file2, file3];
const { success } = await batchUpload(files);
if (success) {
  // 取消第2个上传(假设索引1)
  controllers[1].abort();
}

2. 自定义可取消 Promise(兼容方案)

通过状态标记和回调函数实现可取消的 Promise:

function createCancellablePromise(execute) {
  let isCancelled = false;
  const promise = new Promise((resolve, reject) => {
    const cancel = () => {
      isCancelled = true;
      // 这里可添加清理逻辑
    };
    
    try {
      execute(resolve, reject, cancel);
    } catch (error) {
      reject(error);
    }
  });
  
  promise.cancel = cancel;
  return promise;
}

// 上传任务示例
function uploadWithCancel(file) {
  return createCancellablePromise((resolve, reject, cancel) => {
    // 模拟上传
    const timer = setTimeout(() => {
      if (isCancelled) {
        reject(new Error('上传取消'));
        return;
      }
      resolve(`上传成功: ${file.name}`);
    }, 1000);
    
    // 取消时清除定时器
    cancel(() => {
      clearTimeout(timer);
    });
  });
}

// 批量上传与取消
async function batchUpload(files) {
  const uploadPromises = files.map(uploadWithCancel);
  const results = [];
  
  try {
    // 并行执行但手动处理结果
    for (const promise of uploadPromises) {
      try {
        results.push(await promise);
      } catch (error) {
        if (error.message !== '上传取消') {
          // 非取消错误则中断
          throw error;
        }
        results.push({ error: '已取消' });
      }
    }
    return results;
  } catch (error) {
    // 取消所有剩余任务
    uploadPromises.forEach(p => p.cancel());
    throw error;
  }
}

// 使用示例
const files = [fileA, fileB, fileC];
const uploads = batchUpload(files);
// 取消第二个上传
uploads[1].cancel();

3. 使用 Promise.race 模拟取消(有限场景)

通过 Promise.race 与取消信号结合,适合简单场景:

function uploadFile(file) {
  return new Promise((resolve, reject) => {
    // 模拟上传
    const timeout = setTimeout(() => {
      resolve(`成功: ${file.name}`);
    }, 2000);
    
    // 可取消的上传
    this.cancel = () => {
      clearTimeout(timeout);
      reject(new Error('取消上传'));
    };
  });
}

// 批量上传与取消
async function batchUpload(files) {
  const uploadPromises = files.map(uploadFile);
  const cancelSignals = files.map(() => new Promise(resolve => {}));
  
  try {
    // 并行执行上传与取消信号
    const results = await Promise.all(
      uploadPromises.map((promise, index) => 
        Promise.race([promise, cancelSignals[index]])
      )
    );
    return results;
  } catch (error) {
    // 忽略取消错误
    if (error.message !== '取消上传') {
      throw error;
    }
    return results;
  }
}

// 取消第2个上传
const files = [file1, file2, file3];
const uploadTask = batchUpload(files);
// 500ms后取消第二个上传
setTimeout(() => {
  cancelSignals[1].resolve();
}, 500);

三、问题

1. 问:为什么 Promise.all 不支持直接取消?


  • Promise 设计遵循“承诺一旦创建就不可取消”的原则,主要原因:
    1. 避免不一致状态:取消可能导致部分任务执行、部分未执行,难以保证一致性;
    2. 资源释放复杂性:不同异步任务的资源释放逻辑不同(如网络请求、定时器),无法统一处理;
    3. 场景局限性:取消操作更适合具体场景(如文件上传、网络请求),应由业务层自定义实现。

2. 问:AbortController 如何影响已发出的请求?

    • 浏览器API:对 fetch 请求和 XMLHttpRequest 有效,会中断网络请求并触发 abort 事件;
    • 自定义任务:需手动监听 signal.abort 事件,并添加资源释放逻辑(如清除定时器、取消WebSocket连接);
    • 注意:AbortController 仅取消“正在进行”的任务,无法取消已完成的任务。

3. 问:如何处理 Promise.all 中部分任务取消后的资源释放?

    1. 任务内清理:每个可取消任务需包含清理逻辑(如关闭文件流、释放内存);
    2. 全局清理:在取消时遍历所有任务并调用清理方法:
      // 假设每个任务返回的Promise包含cancel方法
      const uploadPromises = files.map(file => {
        const task = uploadWithCancel(file);
        task.file = file; // 保存上下文
        return task;
      });
      
      // 取消时
      uploadPromises.forEach(task => task.cancel());
      

四、最佳实践与场景选择

方案优势适用场景兼容性
AbortController原生支持、简洁高效浏览器环境、网络请求Chrome 85+、Firefox 90+
自定义可取消Promise灵活适配各种异步任务复杂任务、Node.js环境全环境兼容
Promise.race实现简单、轻量级有限任务数、简单取消需求全环境兼容

五、总结

“在 Promise.all 中取消异步请求需突破 Promise 本身的限制,核心思路是为每个任务添加取消标记或利用原生 AbortController:

  1. 现代方案:使用 AbortController 绑定到异步任务,通过 signal.abort 触发取消,适合网络请求场景;
  2. 通用方案:封装可取消的 Promise,在任务内部处理取消逻辑(如清除定时器、释放资源);
  3. 注意点:取消操作无法回滚已完成的任务,需在设计时考虑任务的幂等性和资源释放。

实际项目中,我在文件上传系统中采用 AbortController 方案,当用户取消批量上传时,通过控制器数组统一中断所有请求,并添加了进度条的实时更新逻辑,确保用户体验的一致性。”