🧠 别急着传!大文件上传里,藏着 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
相关推荐
花笙_几秒前
react打包发到线上报错Minified React error #130
前端·react.js
labixiong3 分钟前
全方位理解跨源资源共享-CORS
前端·后端
AntBlack6 分钟前
闲谈 :AI编程效率反而降低了 ,大家AI 编程的正确姿势到底是什么?
前端·后端·ai编程
倔强青铜三14 分钟前
苦练Python第5天:字符串从入门到格式化
人工智能·python·面试
Mintopia20 分钟前
Three.js 中的噪声与图形变换:一场数字世界的舞蹈
前端·javascript·three.js
Mintopia24 分钟前
计算机图形学漫游:从像素到游戏引擎的奇幻之旅
前端·javascript·计算机图形学
钢铁男儿25 分钟前
C#接口实现详解:从理论到实践,掌握面向对象编程的核心技巧
java·前端·c#
前端的日常1 小时前
以下代码,那一部分运行快
前端
GeGarron1 小时前
Drawing:专注高效画图,让每一次创作都值得被珍藏
前端
梨子同志1 小时前
Vue v-model 指令详解
前端·vue.js