6-18.【架构设计】如何设计 State 才能做到:UI 可预测 + 易调试 + 易回滚?

2 阅读2分钟

一、目标拆解

你希望 State 具备:

  1. UI 可预测

    • 每次 State 改变 → UI 渲染可预期
    • 没有隐藏副作用,避免“偶发性闪烁/错误”
  2. 易调试

    • 可以追踪“谁改了 State、什么时候改的、为什么改的”
  3. 易回滚 / 时间旅行

    • 可以在历史状态间切换
    • 用于 undo/redo 或测试

核心思想:State = 单一真相 + 单向数据流 + 可序列化


二、State 设计原则

1️⃣ 单一状态源(Single Source of Truth)

  • 每个功能 Feature 有唯一 State 对象
  • UI 只能观察 State,不直接改
  • 所有修改通过 Action → Reducer / Effect
UI → Action → Reducer → State → UI
  • 避免双向绑定和多处修改
  • UI 可预测性高

2️⃣ 不可变 / 派生分离

  • State 尽量不可变(Struct)
  • 派生状态不存储在 State 内,只依赖原始 State 计算
struct AppState {
    var users: [User]      // ✅ State
    var searchText: String // ✅ State
    // totalPrice 不存储,而是计算
}

var filteredUsers: [User] {
    users.filter { $0.name.contains(searchText) }
}
  • 好处:

    • 避免双源真相
    • UI 可预测
    • 易调试(只关心真实 State)
    • 可回滚(snapshot 轻量)

3️⃣ 小而扁平,粒度可控

  • 不要把整个 Feature 的状态塞成一坨嵌套对象
  • 结构扁平、按逻辑分域
  • 派生 / 缓存数据放 UI Adapter / ViewModel 层
struct OrderState {
    var items: [Item]
    var selectedItemID: UUID?
}
  • 每个字段变化 → 最小化 UI 失效半径
  • 高频字段放在最下层

4️⃣ Action 可序列化

  • 所有状态修改都通过 Action

  • Action 应该是可枚举 / 可序列化

  • 便于:

    • 调试 trace
    • 时间旅行 / undo
enum OrderAction {
    case addItem(Item)
    case removeItem(UUID)
    case selectItem(UUID?)
}

5️⃣ Reducer 是纯函数

func orderReducer(state: inout OrderState, action: OrderAction) {
    switch action {
    case .addItem(let item):
        state.items.append(item)
    case .removeItem(let id):
        state.items.removeAll { $0.id == id }
    case .selectItem(let id):
        state.selectedItemID = id
    }
}
  • 不依赖外部环境

  • 输入相同 Action + State → 输出总是相同

  • 便于:

    • 可预测 UI
    • 易调试
    • 可回滚

6️⃣ 副作用隔离

  • 不直接在 State / Reducer 中执行网络 / DB / 定时任务
  • 使用 Effect / async Action
  • 副作用触发 Action → 更新 State
Task {
    let data = await api.fetch()
    store.send(.dataLoaded(data))
}
  • 避免“State 一半是 UI,一半是业务副作用”
  • 提高可预测性

三、工程落地策略

1️⃣ 版本化 / 快照

  • TCA 提供 Store 可以 snapshot
  • Redux 可用 time-travel debugger
  • 可调试、可回滚
let snapshot = store.state
// …操作…
store.restore(snapshot)

2️⃣ State 下沉 / 局部化

  • 高频变化字段放局部 View / Row State
  • 减少 diff / 渲染开销
  • 依然保留全局真相作为 Feature State

3️⃣ 派生 State 计算优化

  • 对昂贵 Derived State 使用 memoization 或放 ViewModel 缓存
  • 保证 State 本身轻量
  • 不破坏 UI 可预测性

4️⃣ Action Logging / Trace

  • 每个 Action 发出 → 打日志
  • 可以重放动作
  • 配合 Snapshot → 时间旅行

四、设计心智公式

State = minimal + immutable + serializable
Action → Reducer =唯一修改入口
Derived State = computed or cached near UI
副作用 = Effect / async Action
  • 可预测 UI → 单向数据流 + 纯 Reducer
  • 易调试 → Action 日志 + Snapshot
  • 可回滚 → Immutable State + snapshot

五、真实工程示例

假设一个购物车 Feature:

struct CartState {
    var items: [Item]
    var selectedItemID: UUID?
}

enum CartAction {
    case addItem(Item)
    case removeItem(UUID)
    case selectItem(UUID?)
    case checkout
}

func cartReducer(state: inout CartState, action: CartAction) {
    switch action {
    case .addItem(let item):
        state.items.append(item)
    case .removeItem(let id):
        state.items.removeAll { $0.id == id }
    case .selectItem(let id):
        state.selectedItemID = id
    case .checkout:
        break
    }
}
  • 派生状态:总价 totalPrice 通过 state.items.reduce(...) 计算
  • UI 观察 CartState → List / selection 自动更新
  • Action 日志 + Snapshot → 可调试 + 可回滚

六、总结一句话

State = 系统事实的单一真相 + 不可变 + 受控修改,
Derived State = 从真相即时计算,副作用隔离,Action → Reducer → State 流程串行化,
这样 UI 可预测、易调试、可回滚。