🧠 别急着传!大文件上传里,藏着 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
相关推荐
2501_915106322 小时前
移动端网页调试实战,iOS WebKit Debug Proxy 的应用与替代方案
android·前端·ios·小程序·uni-app·iphone·webkit
柯南二号3 小时前
【大前端】React Native 调用 Android、iOS 原生能力封装
android·前端·react native
睡美人的小仙女1275 小时前
在 Vue 前端(Vue2/Vue3 通用)载入 JSON 格式的动图
前端·javascript·vue.js
yuanyxh5 小时前
React Native 初体验
前端·react native·react.js
大宝贱5 小时前
H5小游戏-超级马里奥
javascript·css·html·h5游戏·超级马里奥
程序视点5 小时前
2025最佳图片无损放大工具推荐:realesrgan-gui评测与下载指南
前端·后端
程序视点6 小时前
2023最新HitPaw免注册版下载:一键去除图片视频水印的终极教程
前端
小只笨笨狗~8 小时前
el-dialog宽度根据内容撑开
前端·vue.js·elementui
weixin_490354348 小时前
Vue设计与实现
前端·javascript·vue.js
GISer_Jing8 小时前
React过渡更新:优化渲染性能的秘密
javascript·react.js·ecmascript