Flow中zip vs combine详解

80 阅读4分钟

一句话心智模型

  • zip = 配对:按“第 i 个对第 i 个”一一配对,短板决定产量(像拉链的齿)。
  • combine = 最新组合:每当任一上游有新值,就用**所有上游的“最新值”**组合发射,只要都出过一次

语义 & 时间线

假设 A 发 a1 a2 a3…,B 发 b1 b2 b3…(→ 表示发射;| 表示完成)

zip(配对)

A:  a1----a2----a3----a4---|
B:  b1--------b2-b3---|
zip: (a1,b1)---(a2,b2)---(a3,b3)   // a4、后续都发不出来
完成:任一上游完成 → 等待把已凑齐的对儿发出 → 结束(“取短”)

combine(最新组合 / combineLatest)

A:  a1----a2----a3----a4---|
B:  b1--------b2-b3---|
combine 的输出时机:
- 先等到 A、B 都至少各发过一次
- 之后 A 或 B 任一再发,就用“另一边的最新值”组合

输出示例:
(a1,b1)  // 两边第一次都到齐时
(a2,b1)  // A 更新,用 B 的最新 b1
(a2,b2)  // B 更新,用 A 的最新 a2
(a3,b2)
(a4,b2)
完成:**所有**上游完成才结束(“取长”);若 B 提前结束,之后 A 继续来,会用 B 的“最后一个值”继续组合。

背压与吞吐

  • zip:快的一侧会被慢的一侧“牵制”。缺对时,快侧在配对边界挂起/排队,整体节奏受限于最慢源。
  • combine不牵制。一旦“首帧到齐”,之后谁更新就触发一次输出;快侧可以高频驱动(用慢侧的“最新值”),吞吐更高,但会跳过慢侧期间的中间值(这是“最新”语义的本意)。

完成与异常

  • 完成

    • zip:任一上游完成 → 把已成对的发完就结束(短板策略)。
    • combine:所有上游完成才结束;即使有的上游先结束,也会用它的最后值与其他仍在发的上游继续组合。
  • 异常

    • 两者:任一上游抛异常 → 下游立即失败并取消其他上游。需要拦截就用 catch { … } 放在操作符之前

何时用谁(场景速选)

诉求/场景选择说明
两条数据“一一对应”配对(第1对第1…)zip如“并行拉两份列表,再按索引配成对”
多状态源合成 UI 状态(谁更新就重算一次combine如滑块值 + 复选框 + 网络状态
需要短板控制节奏,避免快流淹没慢流zip背压天然受“慢流”限制
需要高响应,优先展示最新组合combine快流可频繁驱动,慢流取“最后值”
任一源完成即结束的流水线zip如“拉两段等长流,任何一侧提前完工即收工”
某源完成后仍希望继续用它的最后值combineRx CombineLatest 同款语义

若你的需求根本不是“配对/组合”,而是“把多路事件合成一条通道”——请看 merge()(按时间交叉发射,不做配对或组合)。


放置策略与性能

  • zip { a, b -> … } / combine { a, b -> … } 的大计算,应尽量放在操作符的 lambda 内,并确保可取消(挂起点/isActive/yield()),避免白做工。
  • 对 combine,若下游渲染很贵、上游又很快,可再接 debounce()/sample()/conflate() 或用 collectLatest 以“最新优先 + 可中断”。

代码示例

1) zip:一一配对合表

val ids   = flowOf(101, 102, 103)                  // A
val users = ids.map { id -> api.getUser(id) }      // A'(可另起一支)

val scores = flowOf(88, 92)                        // B(更短)

ids.zip(scores) { id, score ->
    id to score
}.collect { println(it) }                          // 只会有 (101,88), (102,92) 两对

2) combine:多状态合成 UI

val slider: Flow<Int> = sliderChanges()            // A
val checked: Flow<Boolean> = checkboxChanges()     // B
val net: Flow<NetState> = repo.netState()          // C

combine(slider, checked, net) { s, c, n ->
    UiState(value = s, enabled = c, net = n)
}.collectLatest { state ->
    render(state)                                  // 新状态来就中断旧渲染
}

3) combine 在一源完成后的继续发射

val A = flowOf(1, 2, 3)                            // 完成得早
val B = flow {
    emit("x"); delay(100)
    emit("y"); delay(100)
    emit("z")                                      // 后续还会继续
}

A.combine(B) { a, b -> "$a-$b" }
 .collect { println(it) }
// 可能输出:1-x, 2-x, 2-y, 3-y, 3-z …(先等到 A、B 首次都出过;A 完后仍用它的“最后值”与 B 的后续组合,直到 B 也完成)

注意:具体序列取决于时间;但“谁更新就与另一边最新值组合”的规律不变。


易坑提示

  1. 把 combine 当 zip 用:combine 不是一一配对,不会保证“第 i 个配第 i 个”。要严格对齐请用 zip。
  2. zip + 不等长:短流完成后就结束,不会等待长流“凑够长度”;这是预期行为。
  3. combine 首帧:每个上游都至少发过一次后才会有第一个输出;常见做法是在源头用 onStart { emit(initial) } 给初始值。
  4. 取消粒度:在 combine/zip 之后用 collectLatest,确保下游重渲染可中断;上游重算可中断要放在 mapLatest/flatMapLatest 内部。
  5. 错误处理顺序:想兜住上游异常,把 catch { … } 放在 zip/combine 之前;否则下游已经失败。

速记对照

  • zip:配对、等长取短、背压受限、索引对齐。

  • combine:最新组合、取长到全完、快者驱动、需要首帧各自至少一条。

记住这句: “拉链用 zip,拼态用 combine。”