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) 通过取消上游来"快停"。
相关推荐
前端大卫27 分钟前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友28 分钟前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理2 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻2 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front3 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰3 小时前
纯flex布局来写瀑布流
前端·javascript·css
一袋米扛几楼985 小时前
【软件安全】什么是XSS(Cross-Site Scripting,跨站脚本)?
前端·安全·xss
向上的车轮5 小时前
Actix Web适合什么类型的Web应用?可以部署 Java 或 .NET 的应用程序?
java·前端·rust·.net
XiaoYu20025 小时前
第1章 核心竞争力和职业规划
前端·面试·程序员
excel5 小时前
🧩 深入浅出讲解:analyzeScriptBindings —— Vue 如何分析 <script> 里的变量绑定
前端