🧠 别急着传!大文件上传里,藏着 Promise 的高级用法

大文件上传,其实是前端对异步编程掌控力的一次深水区测试。

你以为只是拖个文件框、调个接口而已?

真到了项目里,面对几个 G 的文件,老板一句话:

"不能卡、要能重传、并发别炸服务器、还能暂停",

你顿时发现:这不是前端上传了,这是在搞一套并发调度系统

今天我们就一步步从 实际需求 出发,深入剖析:

  • 🤹‍♂️ Promise 并发控制
  • 🔄 返回 Promise 的本质意义
  • 🧭 上传队列与暂停机制

📦 步骤一:切片上传,异步起手式

首先我们要把大文件分成小块上传:

ini 复制代码
function sliceFile(file: File, chunkSize = 5 * 1024 * 1024): Blob[] {
  const chunks: Blob[] = [];
  let cur = 0;
  while (cur < file.size) {
    chunks.push(file.slice(cur, cur + chunkSize));
    cur += chunkSize;
  }
  return chunks;
}

一张视频动辄几百兆、甚至几个 G,切一切就是上百个切片。

我第一反应就是------Promise.all 干一波并发!

ini 复制代码
const chunks = sliceFile(file);
Promise.all(chunks.map(chunk => uploadChunk(chunk)));

结果浏览器直接卡得像在做压力测试......

❓为啥不行?不是 Promise 并发挺快的吗?

✅ 答案:并发不等于"无限放飞"

  • 浏览器同域并发请求有限制(Chrome 默认 6 个)
  • 你 100 个切片一把梭过去,前面排队、后面卡死,服务器也爆了

我们需要一种更稳健的方式:

"我每次最多只跑 N 个,跑完一个再顶上一个"

这就是我们接下来要实现的:并发控制队列


🔨 步骤二:上传一个切片任务

我们先写上传函数:

typescript 复制代码
function uploadChunk(chunk: Blob, index: number): () => Promise<number> {
  return () =>
    new Promise((resolve, reject) => {
      const form = new FormData();
      form.append('file', chunk);
      form.append('index', index.toString());

      fetch('/upload-chunk', {
        method: 'POST',
        body: form
      })
        .then(res => res.ok ? resolve(index) : reject(new Error('上传失败')))
        .catch(reject);
    });
}

等等,为啥要返回 () => Promise?我不能直接写成这样吗?

typescript 复制代码
function uploadChunk(chunk: Blob, index: number): Promise<number> { ... } // ❓为啥不直接 Promise?

✅ 答案来了:我们要的是"待执行的任务",不是"已经在跑的请求"

队列是一个任务调度器 ,它得有"控制执行"的权力。

如果你直接给它一个已经启动的 Promise,它根本控制不住。

arduino 复制代码
queue.add(uploadChunk(chunk, i)); // ❌ 这时候请求已经发出去了

而这样:

scss 复制代码
queue.add(() => uploadChunk(chunk, i)); // ✅ 是个"待触发的异步任务"

只有队列说:"你现在可以上了",它才会执行。


🎛️ 步骤三:实现 UploadQueue,并发控制器

我们希望它做到:

  • 同时最多执行 N 个任务
  • 一个完成再启动下一个
  • 可暂停/恢复

先实现基础调度功能:

kotlin 复制代码
class UploadQueue {
  private max: number;
  private active = 0;
  private queue: Array<() => Promise<any>> = [];

  constructor(maxConcurrent = 3) {
    this.max = maxConcurrent;
  }

  add(task: () => Promise<any>) {
    this.queue.push(task);
    this.run();
  }

  private run() {
    if (this.active >= this.max || this.queue.length === 0) return;

    const task = this.queue.shift();
    if (!task) return;

    this.active++;
    task()
      .catch(err => console.error('任务失败:', err))
      .finally(() => {
        this.active--;
        this.run(); // 下一个
      });

    // 多个任务并发填满
    this.run();
  }
}

🧪 用法演示

ini 复制代码
const chunks = sliceFile(file);
const queue = new UploadQueue(4);

chunks.forEach((chunk, i) => {
  queue.add(uploadChunk(chunk, i)); // ✅ 注意是 () => Promise
});

❓此时并发真的控制住了吗?

是的,我们控制了 active 数量,每完成一个才补一个,始终同时最多 N 个上传,不会压死浏览器。


🔁 步骤四:上传失败要重试怎么办?

别担心,我们改 uploadChunk,为它加一层重试:

typescript 复制代码
function uploadWithRetry(chunk: Blob, index: number, retry = 3): () => Promise<number> {
  return () =>
    new Promise((resolve, reject) => {
      const attempt = (n: number) => {
        uploadChunk(chunk, index)()
          .then(resolve)
          .catch(err => {
            if (n > 0) {
              console.warn(`第 ${index} 片上传失败,剩余重试 ${n}`);
              attempt(n - 1);
            } else {
              reject(err);
            }
          });
      };
      attempt(retry);
    });
}

现在使用时,只需替换掉任务:

arduino 复制代码
queue.add(uploadWithRetry(chunk, i, 3));

你会发现:真正的链式控制、流程调度,全靠 Promise 的"返回 Promise"能力支撑


⏸️ 步骤五:暂停上传,怎么实现?

这个问题我们一开始也困惑了:

❓"暂停"是能让正在上传的请求停下来吗?

并不能!

JavaScript 中 fetch() 一旦发出请求,除非你用 AbortController,否则无法"中途终止"。


✅ 我们的暂停机制是这样:

  1. 给队列加个 paused 标志
  2. 正在执行的任务继续跑完
  3. 队列 不再启动新的任务,达到"逻辑暂停"效果
kotlin 复制代码
pause() {
  this.paused = true;
}

resume() {
  if (!this.paused) return;
  this.paused = false;
  this.run();
}

private run() {
  if (this.paused || this.active >= this.max || this.queue.length === 0) return;
  ...
}

你点暂停后,当前 3 个任务继续跑,队列停住不再拉新任务

恢复时再继续往下执行。

这就像电梯暂停"进人",但已经上去的人还是要先送完的。


✅ 总结:Promise,不只是"异步工具",而是"异步调度框架"

能力 Promise 的体现
并发调度 控制同时跑几个任务
延迟执行 返回 () => Promise
流程控制 then() 返回另一个 Promise
错误处理 catch + 自定义重试逻辑
状态控制 用暂停标志管理流控

🧩 最后一问:如果我要做"真暂停",怎么办?

可以结合 AbortController,每个上传任务挂上一个 controller,调用 .abort() 即可。

但这会引入新的复杂度:

  • controller 要单独存起来
  • 被取消的请求需要重新 push 回队列
  • UI 状态也要响应变化

🪄 最后一口鸡汤:不要滥用 async/await,它掩盖了真正的异步本质

在这个上传场景中,如果你只会 async/await,那么你很难写出一个结构清晰的上传队列系统。

而当你真正理解 Promise:

  • 你就能写出精确控制并发数的调度器
  • 你就能编排嵌套逻辑,实现"失败重试 + 流程中断"
  • 你就能写出高性能、高可控的上传工具,而不是"看起来能用"的那种 demo
相关推荐
Zuckjet_1 小时前
开启 3D 之旅 - 你的第一个 WebGL 三角形
前端·javascript·3d·webgl
2401_863801461 小时前
探索 12 种 3D 文件格式:综合指南
前端·3d
西阳未落2 小时前
C++基础(21)——内存管理
开发语言·c++·面试
ANYOLY3 小时前
Redis 面试宝典
数据库·redis·面试
珍宝商店3 小时前
前端老旧项目全面性能优化指南与面试攻略
前端·面试·性能优化
bitbitDown3 小时前
四年前端分享给你的高效开发工具库
前端·javascript·vue.js
YAY_tyy3 小时前
【JavaScript 性能优化实战】第六篇:性能监控与自动化优化
javascript·性能优化·自动化
gnip4 小时前
实现AI对话光标跟随效果
前端·javascript
脑花儿5 小时前
ABAP SMW0下载Excel模板并填充&&剪切板方式粘贴
java·前端·数据库
闭着眼睛学算法5 小时前
【华为OD机考正在更新】2025年双机位A卷真题【完全原创题解 | 详细考点分类 | 不断更新题目 | 六种主流语言Py+Java+Cpp+C+Js+Go】
java·c语言·javascript·c++·python·算法·华为od