13-10.【Combine】debounce、throttle、removeDuplicates 的实现原理及使用场景?

2 阅读3分钟

在处理高频触发的事件流(如滚动、点击、输入)时,这三个操作符是 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),视图不应重新绘制。
    • 网络数据刷新:轮询接口返回的数据与本地缓存完全一致时,跳过后续的解析和存储逻辑。

4. 核心特性对比表

特性DebounceThrottleRemoveDuplicates
关注点静止状态频率上限值的内容
触发时机停止发送后的 NN 毫秒固定时间周期内值发生变化的瞬间
是否丢失数据是(丢弃中间态)是(采样)是(丢弃重复态)
依赖参数时间(Scheduler)时间(Scheduler)Equatable 协议

5. 防御式编程警示:Scheduler 的陷阱

debouncethrottle 都需要传入一个 Scheduler(如 RunLoop.mainDispatchQueue.main)。

  • :如果在后台线程产生的流上使用了 debounce 但忘了切换回主线程,后续的 UI 赋值(.assign)会直接导致崩溃或不可预期的行为。

  • 策略:始终明确你的流是在哪个时钟周期下运行的。

    Swift

    inputSubject
        .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) // 确保结果在主线程发出
        .removeDuplicates()
        .sink { ... }
    

总结

  • 想省流量/服务器资源?debounce 处理输入。
  • 想省 CPU/UI 渲染开销?throttle 处理高频交互。
  • 想省逻辑混乱/重复计算?removeDuplicates 处理数据更新。