一句话心智模型
- 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 | 如“拉两段等长流,任何一侧提前完工即收工” |
| 某源完成后仍希望继续用它的最后值 | combine | Rx 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 也完成)
注意:具体序列取决于时间;但“谁更新就与另一边最新值组合”的规律不变。
易坑提示
- 把 combine 当 zip 用:combine 不是一一配对,不会保证“第 i 个配第 i 个”。要严格对齐请用 zip。
- zip + 不等长:短流完成后就结束,不会等待长流“凑够长度”;这是预期行为。
- combine 首帧:每个上游都至少发过一次后才会有第一个输出;常见做法是在源头用 onStart { emit(initial) } 给初始值。
- 取消粒度:在 combine/zip 之后用 collectLatest,确保下游重渲染可中断;上游重算可中断要放在 mapLatest/flatMapLatest 内部。
- 错误处理顺序:想兜住上游异常,把 catch { … } 放在 zip/combine 之前;否则下游已经失败。
速记对照
-
zip:配对、等长取短、背压受限、索引对齐。
-
combine:最新组合、取长到全完、快者驱动、需要首帧各自至少一条。
记住这句: “拉链用 zip,拼态用 combine。”