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