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

相关推荐
千寻girling20 小时前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
xiaoxue..20 小时前
合并两个升序链表 与 合并k个升序链表
java·javascript·数据结构·链表·面试
猿小羽1 天前
AIGC 应用工程师(3-5 年)面试题精讲:从基础到实战的系统备战清单
面试·大模型·aigc·agent·rag
boooooooom1 天前
Pinia必学4大核心API:$patch/$reset/$subscribe/$onAction,用法封神!
javascript·vue.js·面试
试着1 天前
【huawei】机考整理
学习·华为·面试·机试
石去皿1 天前
【嵌入式就业6】计算机组成原理与操作系统核心机制:夯实底层基础
c++·面试·嵌入式
闻哥1 天前
Kafka高吞吐量核心揭秘:四大技术架构深度解析
java·jvm·面试·kafka·rabbitmq·springboot
2501_901147831 天前
面试必看:优势洗牌
笔记·学习·算法·面试·职场和发展
李日灐1 天前
C++进阶必备:红黑树从 0 到 1: 手撕底层,带你搞懂平衡二叉树的平衡逻辑与黑高检验
开发语言·数据结构·c++·后端·面试·红黑树·自平衡二叉搜索树
Bella的成长园地1 天前
为什么c++中的条件变量的 wait() 函数需要配合while 循环或谓词?
c++·面试