一、本质:Cancel 的角色
在响应式系统中(Combine / Rx / TCA / Redux Observable):
每个 Effect / Publisher / Observable 都是时间流
它可能无限、异步、或者高频发生。
Cancel = 主动控制这些流的生命周期。
核心理念:
- 时间流不等于事件流完成
- Cancel = “我不再关心这个流的输出,也不希望它污染 State/UI”
二、Cancel 在架构中的作用
1️⃣ 保持 UI 可预测性
高频事件或异步任务,如果不取消:
用户搜索:Task1
用户快速输入:Task2, Task3
-
如果 Task1 没被取消 → Task1 结果仍会回到 State
-
UI 显示老数据 → 用户感知混乱
-
Cancel 可以保证:
- 只保留最新相关任务的输出
- old task 不污染 State
TCA / Rx 的 switchLatest 就是 Cancel 的具体实现。
2️⃣ 避免资源泄露(性能 / 内存)
- 每个 Publisher / Effect 都是引用对象
- 高频事件 + 长时间未完成 → 占用 ARC / 内存
- Cancel = 主动释放资源,避免内存泄露和 CPU 占用
3️⃣ 保证状态一致性
- Cancel 是架构能力,因为它决定了:
State = 真实可见的事实
- 没有 Cancel → 过期事件修改 State → 双源真相 / UI 不一致
- 有 Cancel → Reducer / State 只处理“当前有效”的事件
三、如果忽略 Cancel,会发生什么
| 问题类型 | 描述 | 示例 |
|---|---|---|
| UI 不可预测 | 异步旧事件更新 UI | 搜索 Task1 返回慢,覆盖 Task3 的结果 |
| 性能消耗 | 高频流占用 CPU / 内存 | TextField 输入流未取消 → 每次输入生成 Publisher |
| 状态混乱 | State 被过期事件污染 | 网络缓存延迟返回旧数据 → Redux Store 错误显示 |
| 难调试 | 异步流未控制,行为随机 | 重复触发请求 → 随机顺序回调 → Bug 难复现 |
工程上,这种现象常被描述为:“响应式外壳 + 命令式内核”,因为外层看起来响应式,但内部流 uncontrolled。
四、架构实践
1️⃣ 使用 Cancelable / Cancellable
- Combine:
AnyCancellable - RxSwift:
Disposable/DisposeBag - TCA:
Effect.cancel(id:)
let task = api.fetch()
.sink { ... }
cancellables.insert(task) // 可以 later cancel
2️⃣ 高频事件 → switchLatest / flatMapLatest
searchPublisher
.map { query in api.search(query) }
.switchToLatest() // 自动取消旧搜索
- 用户只关心最新输入
- 老搜索被取消 → 不污染 State / UI
3️⃣ 生命周期绑定
- View 消失 → 所有绑定流 cancel
- Task / Effect 在 ComponentScope / StoreScope 内绑定,自动释放
五、总结一句话
Cancel 是响应式架构能力的核心:它保证 UI 可预测、State 一致、资源可控。
忽略 Cancel → 响应式链条看似正确,实际内核充满“过期事件 + 竞态 + 内存泄露”,整个系统不可维护。