6-21.【架构设计】响应式链条中,副作用应该放在哪里?为什么很多项目最后会“响应式外壳+命令式内核”?

6 阅读3分钟

一、原则:副作用放的位置

1️⃣ 核心原则

副作用(Effect / IO / 网络 / DB / Timer / 文件操作)应该只在响应式链条的末端产生,并且不污染 State 的纯函数更新逻辑。

换句话说:

Action -> Reducer (纯 State 变更)
       -> Effect (副作用)
  • Reducer = 纯函数,不执行网络 / DB / UI
  • Effect / SideEffect = 可组合、可取消、可追踪的副作用

2️⃣ 为什么要这样

  1. 保证 UI 可预测性

    • Reducer 执行不依赖异步 / 外部副作用 → 输入 State + Action → 输出 State 一定可预测
  2. 保证可测试性

    • 纯 Reducer 可以单元测试,无需网络或文件
    • Effect 可以 mock / 注入测试
  3. 保证可组合 / 可取消

    • 在 Combine / Rx / TCA 中,Effect 可以延迟、并发、取消
    • 避免副作用直接嵌入 body 或 State → UI 失控

二、响应式链条中的副作用位置

1️⃣ 理想链条

UI Event / Timer / Notification
           ↓
       Action (输入)
           ↓
       Reducer (纯函数)
           ↓
       新 State
           ↓
       UI 渲染
           ↓
       Effect (异步副作用)
           ↓
       发回 Action -> Reducer (循环)
  • Effect 是 State 变更之外的输出
  • 可以并发、延迟、取消
  • 保持 State / UI 可预测

2️⃣ 错误做法示例

// 错误:Reducer 内直接发网络请求
func reducer(state: inout AppState, action: Action) {
    switch action {
    case .loadData:
        api.fetch() { data in
            state.items = data   // 副作用直接改变 State
        }
    }
}
  • 问题:

    • Reducer 不再纯 → 难以测试
    • State 改变随异步顺序 → UI 不可预测
    • 并发多个 loadData → 竞态条件
  • 这就是“响应式外壳 + 命令式内核”的开始


三、为什么很多项目退化成“响应式外壳 + 命令式内核”

1️⃣ 初期设计

  • 使用 Combine / Rx 封装 UI → 响应式外壳
  • State / Action / Publisher 形成“看起来响应式”的链条

2️⃣ 业务逻辑复杂

  • 异步请求、缓存、数据库、文件操作越来越多
  • 开发者为了“快” → 在 Reducer / State 内直接写网络 / DB
  • 副作用混入 → Reducer 不再纯 → 状态不可预测

3️⃣ 高频修改 + 并发

  • 响应式链条外的副作用,执行时机不可控
  • UI / State 逻辑开始依赖副作用结果 → 回到命令式思路

🔑 工程规律总结

原则做法结果
副作用外置Reducer 返回 Effect / Publisher可测试、可取消、可组合、UI 可预测
副作用内置Reducer / State 内直接发请求响应式外壳 + 命令式内核 → 状态不可预测,难回溯

核心区别 = 纯函数 Reducer + Effect vs 命令式副作用混入 Reducer


四、实践建议

  1. Reducer = 纯函数

    • 永远只负责计算新 State
    • 不做 IO、网络、文件、定时器、动画
  2. Effect / SideEffect 层

    • 所有异步操作、外部 IO 放这里
    • 可以取消 / debounce / retry
    • 输出 Action → Reducer 再处理
  3. UI 只观察 State / Action

    • 不依赖副作用完成状态
    • 保持渲染可预测
  4. 日志 & 调试

    • 副作用可记录日志、可回放
    • 保持响应式链条透明

五、一句话总结

副作用应该在响应式链条的末端、通过可组合 Effect 触发,Reducer 保持纯函数。
否则,UI 表面响应式,但内核命令式,最终不可预测、不可测试、不可回滚。