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

相关推荐
发现一只大呆瓜13 分钟前
React-手把手带你实现 Keep-Alive 效果
前端·react.js·面试
武藤一雄1 小时前
C#常见面试题100问 (第一弹)
windows·microsoft·面试·c#·.net·.netcore
发现一只大呆瓜1 小时前
Vue - @ 事件指南:原生 / 内置 / 自定义事件全解析
前端·vue.js·面试
koping_wu2 小时前
常用中间件面试汇总:Mysql、Mq、Redis、操作系统、Nacos、Es、Mybatis
mysql·中间件·面试
蒸蒸yyyyzwd2 小时前
后端面试经验
面试·职场和发展
weixin_404157683 小时前
Java高级面试与工程实践问题集(四)
java·开发语言·面试
程序员爱钓鱼4 小时前
Go输出与格式化核心库:fmt包完整指南
后端·面试·go
_饭团4 小时前
C 语言内存函数全解析:从 memcpy 到 memcmp 的使用与模拟实现
c语言·开发语言·c++·学习·算法·面试·改行学it
小金鱼Y4 小时前
前端必看:this 不是玄学!5 大绑定规则帮你永久告别 this 困惑
前端·javascript·面试
tobias.b5 小时前
计算机基础知识-操作系统
考研·面试·职场和发展