1. 什么是回调函数

回调函数(Callback Function),就是把一个函数作为参数传给另一个函数。

外层函数在合适的时机,再去调用这个传进来的函数。

可以理解为:

调用者先把“事情发生后要做什么”告诉被调用者;
被调用者执行到某个阶段后,再反过来调用这个函数通知调用者。

在下载任务中,下载模块负责下载;但“下载进度怎么展示、怎么通知前端”,下载模块不需要自己决定,而是交给调用者通过回调函数定义。


2. StartDownloadOptions 中的回调定义

type StartDownloadOptions = {
  retryCount?: number
  workers?: number
  batchSize?: number
  signal?: AbortSignal

  // 下载进度变化时调用的回调函数
  onProgress?: (progress: DatasetDownloadProgress) => void
}

其中:

onProgress?: (progress: DatasetDownloadProgress) => void

可以拆开理解:

onProgress?:            // 属性名,可选
(progress: DatasetDownloadProgress) // 回调函数接收一个 progress 参数
=> void                 // 回调函数没有返回值

也就是说,调用 startDatasetDownload 时,可以选择传入一个 onProgress 函数。

这个函数将来会接收到一个 DatasetDownloadProgress 类型的进度对象。


3. 调用方如何传入回调函数

await startDatasetDownload(accessUrl, {
  signal: controller.signal,

  onProgress: (progress) => {
    event.sender.send(
      IPC_CHANNELS.datasetDownloadProgress,
      progress,
    )
  },
})

这里传进去的其实是一个匿名箭头函数:

(progress) => {
  event.sender.send(
    IPC_CHANNELS.datasetDownloadProgress,
    progress,
  )
}

它等价于先单独定义一个函数:

function handleProgress(progress: DatasetDownloadProgress): void {
  event.sender.send(
    IPC_CHANNELS.datasetDownloadProgress,
    progress,
  )
}

await startDatasetDownload(accessUrl, {
  signal: controller.signal,
  onProgress: handleProgress,
})

两种写法本质一样。

第一种写法更常见,因为这个函数只在这里使用一次,没有必要额外起名字。


4. 为什么 (progress) 没有写类型?

这里没有写:

(progress: DatasetDownloadProgress) => {
  // ...
}

而是只写:

(progress) => {
  // ...
}

原因是 TypeScript 会根据 onProgress 的类型自动推断参数类型。

前面已经规定:

onProgress?: (progress: DatasetDownloadProgress) => void

因此当你给 onProgress 赋值时,TypeScript 就知道:

progress

必然是:

DatasetDownloadProgress

这叫做 上下文类型推断(Contextual Typing)

所以这两种写法等价:

onProgress: (progress) => {
  console.log(progress.percent)
}
onProgress: (progress: DatasetDownloadProgress) => {
  console.log(progress.percent)
}

通常第一种更简洁。


5. 回调函数什么时候执行?

startDatasetDownload 内部定义了一个辅助函数:

const emitProgress = (message: string): void => {
  options.onProgress?.({
    datasetId,
    total,
    processed,
    downloaded,
    skipped,
    failed,
    offset,
    batchNo: currentBatchNo,
    batchName: currentBatchName,
    percent: toProgressPercent(processed, total),
    outputDir,
    files: Array.from(fileProgressById.values()),
    message,
  })
}

重点是这一句:

options.onProgress?.(...)

它的含义是:

如果调用方传入了 onProgress 回调函数,就执行它;如果没有传入,就什么也不做。

等价于:

if (options.onProgress) {
  options.onProgress({
    datasetId,
    total,
    processed,
    downloaded,
    skipped,
    failed,
    offset,
    batchNo: currentBatchNo,
    batchName: currentBatchName,
    percent: toProgressPercent(processed, total),
    outputDir,
    files: Array.from(fileProgressById.values()),
    message,
  })
}

其中的 ?. 叫做 可选链调用(Optional Chaining Call)


6. progress 参数到底从哪里来的?

调用方定义回调函数时:

onProgress: (progress) => {
  event.sender.send(
    IPC_CHANNELS.datasetDownloadProgress,
    progress,
  )
}

看起来这个 progress 像是“突然出现的”。

实际上,它来自下载函数内部调用回调时传入的对象:

options.onProgress?.({
  datasetId,
  total,
  processed,
  downloaded,
  skipped,
  failed,
  offset,
  batchNo: currentBatchNo,
  batchName: currentBatchName,
  percent: toProgressPercent(processed, total),
  outputDir,
  files: Array.from(fileProgressById.values()),
  message,
})

这里传入的整个对象,就是调用方回调函数中的 progress

也就是说,下面两段代码是一一对应的。

下载函数内部:

options.onProgress?.(进度对象)

调用方传入的函数:

(progress) => {
  // progress 就是“进度对象”
}

7. 执行流程

完整流程可以理解成下面这样:

// 1. 调用方注册“进度更新后要做什么”
startDatasetDownload(accessUrl, {
  onProgress: (progress) => {
    event.sender.send(
      IPC_CHANNELS.datasetDownloadProgress,
      progress,
    )
  },
})
// 2. 下载函数内部,下载过程推进
emitProgress(`已读取 ${dataIds.length} 条下载任务,开始下载 ${batchName}`)
// 3. emitProgress 内部调用 onProgress 回调
options.onProgress?.({
  total,
  processed,
  percent,
  message,
  // 其他进度信息...
})
// 4. 调用方传入的回调被执行
(progress) => {
  event.sender.send(
    IPC_CHANNELS.datasetDownloadProgress,
    progress,
  )
}
// 5. Electron 主进程把进度消息发送给前端页面
event.sender.send(
  IPC_CHANNELS.datasetDownloadProgress,
  progress,
)

最终效果就是:

下载模块更新进度
→ 调用 emitProgress()
→ 执行调用方传入的 onProgress()
→ Electron 主进程通过 IPC 把进度发给渲染进程
→ Vue 页面更新进度条、下载状态、文件列表等 UI。


8. 为什么要这样设计?

下载函数本身只负责:

下载文件
统计成功、失败、跳过数量
计算下载百分比
生成进度数据

但它不应该直接决定:

进度要不要打印到控制台
进度要不要发给 Vue 页面
进度要不要写到日志
进度要不要存数据库

因此通过回调把“进度发生变化后怎么处理”交给调用方。

例如同一个下载函数可以被不同场景复用:

// 场景 1:Electron 中发给前端
onProgress: (progress) => {
  event.sender.send('dataset:download-progress', progress)
}
// 场景 2:命令行程序中打印日志
onProgress: (progress) => {
  console.log(`下载进度:${progress.percent}%`)
}
// 场景 3:后端服务中写入数据库
onProgress: async (progress) => {
  await saveDownloadProgress(progress)
}

下载核心逻辑不用改,只需要替换回调函数。


9. 一句话总结

onProgress: (progress) => {
  // 进度变化后要做的事情
}

这是调用方传入的回调函数。

options.onProgress?.(progress对象)

这是下载函数在合适的时机执行回调函数。

传入的 progress对象,会成为回调函数中的 progress 参数。

更多推荐