首页
/ axios 进度捕获完全指南:onUploadProgress / onDownloadProgress 的实现原理与源码剖析

axios 进度捕获完全指南:onUploadProgress / onDownloadProgress 的实现原理与源码剖析

2026-09-04 15:42:30作者:平淮齐Percy

axios 在浏览器与 Node.js 双环境下都支持捕获上传/下载进度,这是实现上传进度条、下载速度显示等交互功能的基础能力。本文将完整覆盖进度事件的配置用法、事件对象各字段含义、Node.js 流式上传的注意事项,并深入 lib/helpers 下的节流器与速率表源码,讲清"为什么进度事件被限制为每秒 3 次"以及 progressrateestimated 等字段是如何计算出来的。

一、进度捕获的核心机制

axios 同时支持在浏览器Node.js 环境中捕获请求的上传与下载进度。官方文档 docs/pages/advanced/progress-capturing.md(西班牙语版本见 docs/es/pages/advanced/progress-capturing.md)明确指出:进度事件的触发频率被强制限制为每秒 3 次,目的是防止浏览器被大量进度事件淹没、拖垮主线程。

这一"3 次/秒"并非配置项,而是硬编码在事件归约器中的默认频率。从 lib/helpers/progressEventReducer.js 的源码结构看,其签名即为 progressEventReducer(listener, isDownloadStream, freq = 3),三个适配器(xhr / http / fetch)在接线时均使用该默认值,Node.js 的 lib/adapters/http.js 中甚至显式传入 3

配置层面,进度回调在请求配置中通过两个可选字段挂载,对应 TypeScript 定义见 index.d.ts

onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
onDownloadProgress?: (progressEvent: AxiosProgressEvent) => void;

lib/core/mergeConfig.js 中,这两个字段采用 defaultToConfig2 合并策略——即请求级配置优先于实例级默认配置,你可以在 axios.create 时设置全局默认回调,再在单次请求中覆盖。

二、基本用法:捕获上传与下载进度

官方文档给出的标准用法如下,直接对 post 请求配置两个回调即可:

await axios.post(url, data, {
  onUploadProgress: function (axiosProgressEvent) {
    /*{
      loaded: number;
      total?: number;
      progress?: number; // in range [0..1]
      bytes: number; // how many bytes have been transferred since the last trigger (delta)
      estimated?: number; // estimated time in seconds
      rate?: number; // upload speed in bytes
      upload: true; // upload sign
    }*/
  },

  onDownloadProgress: function (axiosProgressEvent) {
    /*{
      loaded: number;
      total?: number;
      progress?: number;
      bytes: number;
      estimated?: number;
      rate?: number; // download speed in bytes
      download: true; // download sign
    }*/
  },
});

事件对象字段详解

axiosProgressEvent 即 TS 中的 AxiosProgressEvent 接口(index.d.ts)。结合文档注释与源码,各字段含义如下:

字段 类型 是否可选 说明
loaded number 必填 已传输的字节数。源码中会先做钳制:Math.max(0, Math.min(rawLoaded, total)),保证 loaded 不会超过 total(见 progressEventReducer.js
total number 可选 总字节数。只有服务端/环境提供了 Content-Length(浏览器中对应 lengthComputable === true)时才有值
progress number 可选 进度比率,取值范围 [0..1],由 loaded / total 计算;无 total 时为 undefined
bytes number 必填 距上一次触发以来的增量字节数(delta),即 loaded - 上次已通报的 loaded
rate number 可选 传输速率(字节/秒),由内部滑动窗口速率表估算;窗口不足时可能为 undefined
estimated number 可选 预计剩余时间(秒),按 (total - loaded) / rate 计算,依赖 ratetotal 同时存在
upload / download boolean 可选 方向标记,用于区分同一个回调被用于哪一侧(源码中两者互斥地置 true,见 progressEventReducer.js
event BrowserProgressEvent 可选 原始底层事件对象(如 XHR 的 progress 事件),仅在浏览器路径下有意义
lengthComputable boolean 必填 标记 total 是否可计算,等价于 total != null

注意:文档注释中没有列出 eventlengthComputable 两个字段,但它们确实存在于 TS 接口定义与 progressEventReducer 的产出数据中(progressEventReducer.js),在浏览器端可依赖 lengthComputable 判断进度条是否"有总量"。

实战:显示上传进度条

结合上表字段,一个典型的上传进度日志实现:

const { data } = await axios.post(SERVER_URL, file, {
  onUploadProgress: ({ progress, rate, estimated }) => {
    const percent = ((progress ?? 0) * 100).toFixed(1);
    const kbps = rate ? (rate / 1024).toFixed(1) : '--';
    console.log(`上传中 [${percent}%] 速率 ${kbps} KB/s,预计剩余 ${estimated?.toFixed(1)}s`);
  },
});

三、Node.js 流式上传的进度捕获

在 Node.js 中,你还可以将上传与下载进度事件直接对接到一个 readable stream 的场景中捕获。这在需要对流式上传展示自定义进度时非常有用(官方文档给出的示例):

const { data } = await axios.post(SERVER_URL, readableStream, {
  onUploadProgress: ({ progress }) => {
    console.log((progress * 100).toFixed(2));
  },

  headers: {
    "Content-Length": contentLength,
  },

  maxRedirects: 0, // avoid buffering the entire stream
});

这个示例中有两个关键细节,直接对应源码中的处理逻辑:

  1. 必须提供 Content-Length。从 lib/adapters/http.js 的源码结构看,http 适配器通过 utils.toFiniteNumber(headers.getContentLength()) 取出请求体的总长,并用它构造 progressEventDecorator(contentLength, ...)——装饰器会把 total 固定为该值,回调中才会出现可计算的 progress。没有它,total/progress 均为 undefined,你只能拿到 loaded 的单调递增值。
  2. 建议设置 maxRedirects: 0 禁用重定向。官方文档(danger 级别警告)明确说明:Node.js 环境下 follow-redirects 包在发生重定向时会将整个流缓冲到内存(RAM)中,且不遵循"背压(backpressure)"算法——大文件流式上传遇到 3xx 重定向可能导致内存暴涨。禁用重定向后,3xx 会直接作为响应返回,流可以按背压逐块发送。

FormData 上传进度的限制

文档同时给出一条 warning:目前 FormData 的上传进度捕获在 Node.js 环境中不可用。其根源在于从源码结构看,http 适配器的上传进度依赖将请求体统一转换为 stream 后监听 progress 事件(lib/adapters/http.js),而 Node 端 form-data 包生成的 multipart/form-data 边界流没有可靠的总长度信号,total 无法确定,因此该能力尚未支持。若需要大表单进度,可考虑改用二进制流 + Content-Length 的方式。

四、源码深潜:进度事件是如何产生并节流的

三个适配器共用同一套进度事件工具链,核心在 lib/helpers/progressEventReducer.jslib/helpers/throttle.jslib/helpers/speedometer.js

4.1 节流器:leading + trailing 边沿的每秒 3 次

throttle.js 实现的是一个带尾随调用的节流装饰器:

  • 时间阈值 threshold = 1000 / freqfreq = 3 时约为 333ms
  • 若两次调用间隔 >= threshold立即同步触发(leading 边沿);
  • 否则缓存最后一次参数 lastArgs,并在 threshold - passed 毫秒后通过 setTimeout 补发一次(trailing 边沿);
  • 返回 [throttled, flush] 元组,flush 用于在流结束前把缓存的最后一次事件强制刷出。

这就是为什么进度条的最后一帧总能到达 100%——即使最后一次网络事件落在节流窗口内,也会在结束时被 flush 出来。

4.2 速率表:50 个样本的滑动窗口

rate 字段(字节/秒)来自 speedometer.js:它维护一个最多 50 个样本的环形缓冲,每个样本记录一次 bytes 增量及其时间戳。关键行为:

  • 首次调用到当前时间不足 min(progress 场景传入 250ms,见 progressEventReducer.js)时返回 undefined,所以请求最初约 1/4 秒内 rate/estimated 可能为空
  • 达到窗口后,速率 = 窗口内总字节数 / 窗口经过时间(毫秒),即 Math.round((bytesCount * 1000) / passed)

estimated(预计剩余秒数)则是在 progressEventReducer.js 中由 rate && total ? (total - loaded) / rate : undefined 推导的。

4.3 浏览器路径:XHR 的 progress 事件

lib/adapters/xhr.js 中,适配器为 XHR 对象挂接节流后的监听器:

// Handle progress if needed
if (onDownloadProgress) {
  [downloadThrottled, flushDownload] = progressEventReducer(onDownloadProgress, true);
  request.addEventListener('progress', downloadThrottled);
}

// Not all browsers support upload events
if (onUploadProgress && request.upload) {
  [uploadThrottled, flushUpload] = progressEventReducer(onUploadProgress);
  request.upload.addEventListener('progress', uploadThrottled);
  request.upload.addEventListener('loadend', flushUpload);
}

两个值得注意的实现细节:

  • request.upload 兼容性检查:并非所有浏览器都支持上传进度事件,源码会先判断 request.upload 存在才注册,不支持的浏览器中 onUploadProgress 静默不触发;
  • loadend 时 flush:上传结束时强制把节流窗口内缓存的最后一个事件刷出,保证 loaded === total 的终态事件一定送达。

4.4 Node.js 路径:transform stream + 异步调度

http 适配器(lib/adapters/http.js)的上传路径是:请求体(无论 Buffer 还是 Stream)被包装进 AxiosTransformStream 管道,该流在每次 write 后发出 progress 事件;回调再经过三层装饰:

progressEventDecorator(                       // 注入固定的 total(来自 Content-Length)
  contentLength,
  progressEventReducer(                        // 节流 3 次/秒 + 计算 rate/estimated
    asyncDecorator(onUploadProgress, scheduleProgress),  // 回调放到微任务队列执行,避免阻塞 IO
    false,
    3
  )
)
  • asyncDecoratorprogressEventReducer.js)把用户回调放入 utils.asap 调度,避免在 stream 的写路径中同步执行业务代码而阻塞背压;
  • flushOnFinish 监听流结束,把最后一次未触发的事件刷出,与浏览器端的 loadend flush 逻辑对齐。

下载路径(lib/adapters/http.js)完全同构:响应流 res 串接一个 AxiosTransformStreamtotal 取自响应头的 content-length,进度监听器同样按 freq = 3 节流。fetch 适配器(lib/adapters/fetch.jslib/adapters/fetch.js)也复用同一套 progressEventDecorator / progressEventReducer,保证三种适配器下事件字段语义一致。

补充:Node.js 中该管道还与带宽限制能力复用——当同时配置 maxRate(上传/下载限速)时,AxiosTransformStream 承担限速职责,进度事件照常发出。maxRate 的用法详见 docs/pages/advanced/rate-limiting.md,它仅在 Node.js HTTP 适配器下生效,浏览器中无效。

4.5 测试佐证

仓库中的冒烟测试 tests/smoke/esm/tests/progress.smoke.test.js 验证了上述行为:

  • 上传用例:以 Readable.from(payload) 作为请求体并显式设置 Content-Length,断言收到的最后一个事件满足 { loaded: total, total, upload: true }——验证了"终态 flush"与方向标记;
  • 下载用例:通过自定义 transport 响应头携带 content-length,断言最后一个事件为 { loaded: total, total, download: true },且 response.data 内容完整。

浏览器端的对应测试见 tests/browser/progress.browser.test.js,http/fetch 适配器的单测见 tests/unit/adapters/http.test.jstests/unit/adapters/fetch.test.js,可进一步查看各环境下的断言细节。

五、要点回顾

  1. onUploadProgress / onDownloadProgress 在浏览器与 Node.js 双端可用,事件频率被硬编码节流为每秒 3 次(leading + trailing 边沿),且结束时保证 flush 出 100% 终态事件;
  2. total/progress 是否有值取决于环境能否给出总量:浏览器看 lengthComputable,Node.js 看 Content-Length(上传)/ content-length(响应头);
  3. rateestimated 基于 250ms 起步的 50 样本滑动窗口估算,请求初期可能为 undefined,UI 层需要兜底;
  4. Node.js 流式上传务必显式设置 Content-Length,并建议 maxRedirects: 0 以避免 follow-redirects 将整个流缓冲进内存;
  5. FormData 上传进度在 Node.js 环境暂不支持,浏览器中 XHR upload 事件缺失时上传回调静默不触发。
登录后查看全文
热门项目推荐
相关项目推荐