TypeScript 回调函数:以下载进度通知为例
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 参数。
更多推荐

所有评论(0)