Flow 里的上游/下游

1) 上游 / 下游是啥

  • 上游 (upstream) :更靠近数据来源的一段(如 flow {}、emitAll(...)、map、filter 等在链条中更左边的部分)。

  • 下游 (downstream) :更靠近收集者的一段(如 onEach、collect、UI 渲染等在链条更右边的部分)。

  • 相对概念 :对某个操作符而言,位于它左侧 的是它的上游,右侧的是它的下游。

数据从左往右流:source → ...operators... → collect

2) 为啥要这样设计

  • 背压与解耦:上游是"生产者",下游是"消费者"。通过操作符(如 buffer)在两段之间插入"缓冲边界",避免一方拖慢另一方。

  • 协作式取消 :下游取消/完成会反向取消上游(如 take 提前结束会取消上游继续产出)。

  • 上下文隔离 :不同阶段用不同线程/调度器(flowOn 把它左侧的上游切到 Dispatchers.Default,而下游仍在原上下文)。

  • 异常边界 :像 catch {} 只拦截它左侧(上游)抛出的异常,保证异常传播有边界、可推理。

3) 为什么有些操作影响上游,有些影响下游

本质:操作符在链中的位置 + 操作符的语义

你把它放在链条中的哪里,它就对它左侧(上游)或右侧(下游)产生作用;而不同操作符被设计成只拦截/切换/限流某一方向。

常见操作符方向性

只影响"上游"的典型

  • flowOn(context) :把它左侧的上游搬到指定调度器执行;右侧不受影响。
  • catch {} :只捕获它左侧上游抛出的异常;对右侧(下游)的异常无能为力。
  • retry / retryWhen :只对上游的失败重试。
  • onStart {} :在向上游请求之前触发(可先发"加载中")。
  • take(n) :当下游已拿到 n 个值就取消上游继续生产(通过协作取消)。
scss 复制代码
flow { emit(heavy()) }          // 上游
  .map { work(it) }             // 仍在上游
  .flowOn(Dispatchers.Default)  // ↑ 上述两步都切到 Default
  .onEach { render(it) }        // 下游(仍在收集端上下文)
  .catch { e -> log(e) }        // 只抓到左侧(上游)的异常
  .collect()

关键:flowOn 的位置决定哪些"左侧操作"被搬到新上下文。

更偏"下游"的典型

  • collect {} / launchIn(scope) :位于链末端,消耗数据;其抛出的异常不被前面的 catch 捕捉。
  • onEach {} :常作为下游副作用(日志、埋点、UI 刷新)。若在 catch 右侧,catch 不会捕它抛出的异常。
  • collectLatest { } :新值到来时取消正在进行的下游处理(不是取消上游生产),常用于只处理"最新 UI 任务"。
scss 复制代码
flowOf(1,2,3)
  .onEach { delay(100) }         // 这一步如果抛异常,取决于它位于 catch 的哪侧
  .catch { e -> log("catch不到右侧异常") }
  .collect { println(it) }

"切分边界/背压相关"的典型

  • buffer(capacity) :在当前位置 插入一个通道缓冲边界。其左侧上游可以先产出、右侧下游慢慢处理,从而解耦速度
  • conflate() :下游慢的时候丢弃中间值只保留最新(上游仍可快产出,减少积压)。
  • debounce(t) / sample(t) :在所处位置起作用,属于节流/抖动,影响流经此处的元素形态与节奏。
  • flatMapLatest :当上游发出新的"子流"时,取消上一个子流的下游消费;对先前子流的上游也会被取消(协作式)。
scss 复制代码
eventsFlow()
  .buffer(64)           // 在此处建立缓冲:上游快产,下游慢吃
  .conflate()           // 若下游仍慢,则只保留最新事件
  .collect { handle(it) }

4) 放置位置改变语义(两个高频坑)

A.flowOn放哪里,哪里(左侧)才会换线程

scss 复制代码
// heavy() 与 map 都在 Default 线程
flow { emit(heavy()) }
  .map { transform(it) }
  .flowOn(Dispatchers.Default)
  .collect { render(it) }  // render 在收集端上下文(通常 Main)
scss 复制代码
// 只有 flow{} 在 Default,map 在下游(例如 Main)
flow { emit(heavy()) }
  .flowOn(Dispatchers.Default)
  .map { transform(it) }    // 仍在收集端上下文
  .collect { render(it) }

B.catch只抓左侧异常

scss 复制代码
flow { emit(value()) }
  .map { mayThrow(it) }
  .catch { e -> log("能抓到 map/上游异常: $e") }
  .onEach { sideEffectThatMayThrow(it) }
  .collect() // onEach 抛出的异常,catch 抓不到

若要抓下游异常,请把 try/catch 包在 collect 外层,或把可能抛错的操作移到 catch 左侧。

5) 取消与背压(理解"为什么这么设计"的关键)

  • 取消向上游传播 :下游不再需要数据(如 take 达到条件、collectLatest 来新值、UI 销毁)→ 上游被取消,避免无谓计算/解码/IO。

  • 背压默认是"挂起" :下游慢,默认会让上游在挂起点等待;加 buffer/conflate 改变这条边界的行为。

  • 冷流 :Flow 直到 collect 才启动;每次收集都是一次新的上游

6) 速查表(记住这几条就够用)

  • 方向规则 :操作符只对它左边(上游)或经过它的元素生效;collect 在最右。
  • 线程切换 :用 flowOn,且它只影响左边;不要在 flow {} 里随意 withContext(会违反 Flow 不变式)。
  • 异常 :catch 只抓左侧上游异常;下游异常自己 try/catch。
  • 背压:buffer = 解耦;conflate/sample/debounce = 限频;collectLatest = 取消旧的下游处理。
  • 提前结束:take(n) 通过取消上游来"快停"。
相关推荐
日月晨曦7 小时前
React 在线 playground 实现指南:让代码在浏览器里「原地爆炸」的黑科技
前端·react.js
金州_拉文7 小时前
uniapp
前端·uni-app
鹏程十八少7 小时前
10. Android <卡顿十>高度封装Matrix卡顿, 修改Matrix源码和发布自己的插件
前端
写代码的stone7 小时前
antd时间选择器组件体验优化之useLayoutEffect 深度解析:确保 DOM 更新时序的关键机制
前端
Lazy_zheng7 小时前
8 个高频 JS 手写题全面解析:含 Promise A+ 测试实践
前端·javascript·面试
子轩学长说7 小时前
Nano banana极致能力测试,不愧为P图之神~
前端
月出7 小时前
社交登录 - Twitter(前后端完整实现)
前端·twitter
万添裁7 小时前
C++的const_cast
开发语言·前端·javascript
小桥风满袖7 小时前
极简三分钟ES6 - 解构赋值
前端·javascript