在 Combine 中,Subject 是连接命令式代码(如按钮点击、代理回调)与响应式世界的桥梁。PassthroughSubject 和 CurrentValueSubject 的核心区别在于它们对“状态”的持有能力。
1. 核心区别:状态与首个值
| 特性 | PassthroughSubject | CurrentValueSubject |
|---|---|---|
| 是否持有状态 | 否。它只是一个“管道”。 | 是。它持有一个“当前值”。 |
| 初始化要求 | 不需要初始值。 | 必须提供初始值。 |
| 订阅瞬间 | 保持沉默,直到下一次 send()。 | 立即收到当前持有的最新值。 |
| 同步访问 | 无法直接获取当前值。 | 可以随时通过 .value 同步读取/修改。 |
2. PassthroughSubject:即时的“事件”
本质:传递瞬时发生的信号。
它就像一个门口的门铃。按下去的时候响一声,如果你当时不在家(没订阅),你就永远错过了这一声铃响。它不记录过去发生的任何事情。
-
应用场景:
- 用户交互:按钮点击、屏幕轻扫。
- 通知/回调:简单的事件通知(如
userDidLogout)。 - 单次触发:不需要知道历史状态,只关心“现在发生了什么”。
3. CurrentValueSubject:持久的“状态”
本质:代表一个随时间变化的值。
它就像一个室内的温度计。你任何时候看它,它都有一个当前数值。当你开始观察它时,它会立刻告诉你当前的温度,随后如果有变化,它会持续更新。
-
应用场景:
- 数据流中心:ViewModel 中的 UI 状态(如
isLoggedIn,userName)。 - 配置管理:当前的主题颜色、用户权限级别。
- 同步检查:当你需要在代码中直接写
if subject.value == .loading这种逻辑时。
- 数据流中心:ViewModel 中的 UI 状态(如
4. 深度对比:选择哪一个?
场景 A:搜索框输入
如果你只想在用户打字时触发搜索:
- 选 PassthroughSubject。因为之前的搜索词通常不再重要,你只关心“当前这一刻”输入了什么。
场景 B:设置页面的开关状态
- 选 CurrentValueSubject。因为当你打开这个页面时,UI 必须知道开关当前是开还是关,从而进行初始化渲染。
5. 防御式设计建议:封装 Subject
无论使用哪种 Subject,为了保护模块边界,永远不要直接暴露 Subject 给外部。
Swift
class UserViewModel {
// 1. 内部私有:具有主动发送权
private let stateSubject = CurrentValueSubject<ViewState, Never>(.idle)
// 2. 外部只读:抹除 send() 能力,仅允许订阅
var state: AnyPublisher<ViewState, Never> {
stateSubject.eraseToAnyPublisher()
}
}
总结
- 如果你需要发送一个动作,用
PassthroughSubject。 - 如果你需要维护一个数据,用
CurrentValueSubject。