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

相关推荐
excel4 小时前
Vue3 EffectScope 源码解析与理解
前端·javascript·面试
UrbanJazzerati4 小时前
考研数学:求根公式
面试
UrbanJazzerati5 小时前
考研数学:巧用柯西不等式
面试
hanxiaozhang20185 小时前
Netty面试重点-1
网络·网络协议·面试·netty
刀客1235 小时前
C++ 面试总结
开发语言·c++·面试
序属秋秋秋6 小时前
《C++进阶之C++11》【智能指针】(下)
c++·笔记·学习·面试·c++11·智能指针·新特性
序属秋秋秋7 小时前
《C++进阶之C++11》【智能指针】(上)
c++·笔记·学习·面试·c++11·智能指针·新特性
迷知悟道11 小时前
java基础知识之接口---超详细(保姆级)
面试
UrbanJazzerati12 小时前
一文带你了解表语
面试
聪明的笨猪猪14 小时前
Java “并发工具类”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试