Kotlin中debounce(t)详解

1) 它到底做什么?

  • 语义 :当上游连续快速 emit 时,等待最近一次发射后的空闲期达到 t 才把"最后那个值"往下游发。期间若又来新值,就重置计时

  • 结论 :只在静默(no new item)持续 ≥ t 时发射一次"尾值"(trailing edge)。

时序示意(t=300ms):

lua 复制代码
--a---b----c-------------      upstream
          <---300ms---> emit c

2) 常用签名与行为

kotlin 复制代码
fun <T> Flow<T>.debounce(timeoutMillis: Long): Flow<T>
// 也有 Duration 版本与"按值决定超时"的版本:
fun <T> Flow<T>.debounce(timeout: Duration): Flow<T>
fun <T> Flow<T>.debounce(timeoutSelector: (T) -> Duration): Flow<T>
  • flush 行为 :上游完成(或被取消)时,会把挂起的最后一个值补发(如果存在)。

  • 可取消:内部基于 delay,遇到取消会立刻停止等待。

3) 典型用途

  • 文本输入 → 搜索请求:降低频繁网络调用。

  • 高频进度/位置回调 → UI 渲染:减少无意义重绘。

  • 传感器/点击抖动去噪。

4) 放置位置(很关键)

把 debounce() 放在昂贵操作前面,让昂贵操作只在"稳定意图"出现时执行:

scss 复制代码
textChanges()
  .debounce(300)
  .distinctUntilChanged()
  .flatMapLatest(repo::search)  // 只在停顿后触发一次
  .collectLatest(::render)

若把重活放在 debounce 之前(如 map { slow() }),每次输入仍会做重活,达不到"降频"的目的。

5) 和其他操作符怎么选?

诉求 选择
停顿后只发一次尾值(抗抖) debounce(t)
按固定周期取样(不看静默) sample(period)
只要最新,丢中间,但不取消下游 conflate()
来新就取消旧处理,永远只跑最新 mapLatest / collectLatest
限制队列、背压不丢 buffer(n, SUSPEND)

常用组合:debounce(300).distinctUntilChanged().flatMapLatest(...)
debounce 不会去重相等值,需要再接 distinctUntilChanged()。

6) 动态超时与"首发立即、其后防抖"

  • 按值决定超时:例如空字符串立刻发,其它 300ms:
scss 复制代码
textChanges().debounce { s ->
  if (s.isBlank()) 0.milliseconds else 300.milliseconds
}
  • 首发立即,之后防抖(一个常见 UX):
scss 复制代码
textChanges()
  .onStart { emit(initialQuery) } // 首发
  .debounce(300)
  .distinctUntilChanged()
  .flatMapLatest(repo::search)
  .collectLatest(::render)

7) 常见坑

  • 以为能限速上游 :debounce 只是推迟下游 ,并不会让上游"慢下来";若需要限速/减工,结合 sample()/conflate()/把重活放到 mapLatest/flatMapLatest 里

  • 放错顺序:slow().debounce() 无效于省工;应 debounce().slow()。

  • 重复值仍会发:除非再 distinctUntilChanged()。

  • CPU 密集下游看不到"取消" :若配 collectLatest,下游需有挂起点或 yield()/isActive 检查,取消才及时生效。

8) Compose/Android 实战片段

Compose TextField 搜索:

kotlin 复制代码
@Composable
fun Search(vm: SearchVM = viewModel()) {
  val query by vm.query.collectAsState()
  TextField(value = query, onValueChange = vm::updateQuery)

  LaunchedEffect(Unit) {
    snapshotFlow { query }                  // 收集状态快照
      .debounce(300)
      .distinctUntilChanged()
      .flatMapLatest { q -> vm.repo.search(q) }
      .collectLatest { result -> vm.uiState.value = result }
  }
}

进度条防抖渲染:

scss 复制代码
downloader.progressFlow() // 0..100 高频
  .debounce(50)           // 降低 UI 刷新
  .collectLatest(progressBar::setProgress)

9) 单元测试要点

使用 kotlinx-coroutines-test:

scss 复制代码
@RunTest
fun `debounce emits trailing`() = runTest {
  val values = mutableListOf<Int>()
  val job = backgroundScope.launch {
    flow {
      emit(1); delay(100)
      emit(2); delay(100)
      emit(3) // 然后静默
    }.debounce(300).toList(values)
  }
  advanceUntilIdle()
  assertEquals(listOf(3), values) // 只收尾值 3
  job.cancel()
}

一句话总结

debounce(t) = "静默 t 后只发尾值" 。把它放在慢/贵操作前,常与 distinctUntilChanged、flatMapLatest/collectLatest 组合,用于输入去抖、进度/渲染降频。

相关推荐
南北是北北2 小时前
详解Flow的collectLatest { ... }
面试
南北是北北2 小时前
Flow中的buffer详解
面试
小高0072 小时前
🤔函数柯里化:化繁为简的艺术与实践
前端·javascript·面试
Sherry0072 小时前
【译】掌握 Flexbox 的终极指南:从烤肉串到鸡尾酒香肠的布局哲学
css·面试·flexbox
绝无仅有3 小时前
大厂Redis高级面试题与答案
后端·面试·github
绝无仅有3 小时前
面试问题之导致 SQL 查询慢的原因及优化建议
后端·面试·github
在未来等你5 小时前
Kafka面试精讲 Day 18:磁盘IO与网络优化
大数据·分布式·面试·kafka·消息队列
程序员清风5 小时前
滴滴三面:ZGC垃圾收集器了解吗?
java·后端·面试
南北是北北5 小时前
Flow中的背压与并发
面试