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 组合,用于输入去抖、进度/渲染降频。

相关推荐
Haooog12 小时前
Docker面试题(不定时更新)
java·docker·面试
feathered-feathered12 小时前
Redis基础知识+RDB+AOF(面试)
java·数据库·redis·分布式·后端·中间件·面试
a程序小傲12 小时前
小红书Java面试被问:java创建对象有哪些方式?
java·开发语言·面试
想用offer打牌14 小时前
一站式了解数据库三大范式(库表设计基础)
数据库·后端·面试
小橙编码日志15 小时前
MongoDB深入与实战:基于SQL的对照解析
后端·面试
PineappleCoder18 小时前
WebP/AVIF 有多香?比 JPEG 小 30%,<picture>标签完美解决兼容性
前端·面试·性能优化
玩具猴_wjh18 小时前
面试问题相关回答
面试·职场和发展
Haooog19 小时前
微服务篇面试题(不定时更新)
微服务·面试·架构
Croa-vo19 小时前
NVIDIA 2025 Deep Learning & Systems 岗位面试复盘 | C++并发与底层架构难度解析
c++·深度学习·面试
知其然亦知其所以然19 小时前
JavaScript 变量的江湖恩怨:一篇文章彻底讲清楚
前端·javascript·面试