在处理高频触发的事件流(如滚动、点击、输入)时,这三个操作符是 Combine 中最重要的**“流量过滤器”**。它们通过不同的策略减少不必要的计算和 UI 刷新,从而提升 App 的性能。
1. Debounce (防抖)
实现原理:
debounce 会在收到一个值后开启一个倒计时器。如果在计时结束前收到了新值,则重置计时器。只有当计时器顺利归零(即一段时间内没有新值产生)时,它才会发出最后收到的那个值。
核心逻辑: “等手停了再执行”。
-
使用场景:
- 搜索框实时搜索:用户快速输入 "SwiftUI" 时,不希望每打一个字就发一次网络请求,而是等用户停顿 300ms 后再发。
- 窗口大小调整:用户拖拽改变窗口大小时,只在拖拽停止后重新计算复杂的布局。
2. Throttle (节流)
实现原理:
throttle 会按照固定时间窗口对流进行切分。在一个窗口期内,无论收到多少个值,它都只允许其中一个值通过。你可以通过 latest 参数决定是发该窗口内的第一个(latest: false)还是最后一个(latest: true)值。
核心逻辑: “控制最高触发频率”。
-
使用场景:
- 抢购按钮点击:防止用户 1 秒内点击 10 次提交订单,强制设定 1 秒内只响应一次。
- 位置信息更新:GPS 每秒产生 60 次数据,但 UI 只需每秒更新一次位置,减少能耗。
- 滚动加载(Infinite Scroll) :在用户滚动时,每隔 200ms 检查一次是否触底,而不是每一像素都检查。
3. RemoveDuplicates (去重)
实现原理:
removeDuplicates 内部持有一个**“前一个值”**的引用。每当有新值进入时,它会使用 Equatable 协议或自定义闭包将新值与旧值对比。如果两者相等,则直接丢弃新值;如果不等,则发出新值并更新内部引用。
核心逻辑: “只要不变化,就不响应”。
-
使用场景:
- 状态驱动渲染:ViewModel 中的状态虽然被多次更新,但内容没变(例如从
loading重复设置为loading),视图不应重新绘制。 - 网络数据刷新:轮询接口返回的数据与本地缓存完全一致时,跳过后续的解析和存储逻辑。
- 状态驱动渲染:ViewModel 中的状态虽然被多次更新,但内容没变(例如从
4. 核心特性对比表
| 特性 | Debounce | Throttle | RemoveDuplicates |
|---|---|---|---|
| 关注点 | 静止状态 | 频率上限 | 值的内容 |
| 触发时机 | 停止发送后的 毫秒 | 固定时间周期内 | 值发生变化的瞬间 |
| 是否丢失数据 | 是(丢弃中间态) | 是(采样) | 是(丢弃重复态) |
| 依赖参数 | 时间(Scheduler) | 时间(Scheduler) | Equatable 协议 |
5. 防御式编程警示:Scheduler 的陷阱
debounce 和 throttle 都需要传入一个 Scheduler(如 RunLoop.main 或 DispatchQueue.main)。
-
坑:如果在后台线程产生的流上使用了
debounce但忘了切换回主线程,后续的 UI 赋值(.assign)会直接导致崩溃或不可预期的行为。 -
策略:始终明确你的流是在哪个时钟周期下运行的。
Swift
inputSubject .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) // 确保结果在主线程发出 .removeDuplicates() .sink { ... }
总结
- 想省流量/服务器资源? 选
debounce处理输入。 - 想省 CPU/UI 渲染开销? 选
throttle处理高频交互。 - 想省逻辑混乱/重复计算? 选
removeDuplicates处理数据更新。