【The Composable Architecture (TCA) 源码解析】07 - Reducer

277 阅读3分钟

此源码解析适用于 0.41.0 之前的版本。最新的源码已经把 Reducer 重构成了一个协议 ReducerProtocol,但核心的实现方法还是离不开这个版本的源码。

Reducer 描述了在给定 Action 的情况下,如何将应用程序的当前状态演变为下一个状态,并描述 Store 稍后应该执行什么 Effect

Reducer 有三个泛型:

  • State:保存应用程序当前状态的类型。
  • Action:促使应用程序状态发生改变的类型。
  • Environment:包含产生 Effect 所需的所有依赖的类型。例如 API、analytics 等等。

注意:Effect 在哪个线程输出很重要。Effect 的输出是马上发送给 Store的,Store 并不是线程安全的。这意味着所有 Effect 的输出必须在同一线程;并且如果 Store 是用于驱动 UI,则必须在主线程。

上面所提到的问题,仅存在于使用 Combine 框架来创建 Effect的情况。如果使用 Swift 并发和 Effect 的三种方法 .task.run.fireAndForget,则线程问题由 Swift 自动处理。

Reducer

public struct Reducer<State, Action, Environment> {
  private let reducer: (inout State, Action, Environment) -> Effect<Action, Never>

  // 使用 `reducer` 闭包创建 `Reducer`。
  // `reducer` 有三个参数: `state`, `action` 和 `environment`. 其中 `state` 是 `inout` 的,
  // 可以直接在里面对其进行修改。
  // `reducer` 必须返回一个 `Effect`,通常是用 `environment` 来创建. 如果不需要做任何操作,则返回 `.none`。
  // 例如:
  /// ```swift
  /// struct MyState { var count = 0, text = "" }
  /// enum MyAction { case buttonTapped, textChanged(String) }
  /// struct MyEnvironment { var analyticsClient: AnalyticsClient }
  ///
  /// let myReducer = Reducer<MyState, MyAction, MyEnvironment> { state, action, environment in
  ///   switch action {
  ///   case .buttonTapped:
  ///     state.count += 1
  ///     return environment.analyticsClient.track("Button Tapped")
  ///
  ///   case .textChanged(let text):
  ///     state.text = text
  ///     return .none
  ///   }
  /// }
  /// ```
  public init(_ reducer: @escaping (inout State, Action, Environment) -> Effect<Action, Never>) {
    self.reducer = reducer
  }

  // 一个不对 `State` 作任何修改和没有任何副作用的 `Reducer`
  public static var empty: Reducer {
    Self { _, _, _ in .none }
  }
}

组合 Reducer

// 通过按顺序在 `State` 上执行每个 `Effect`,并合并所有 `Effect`s,将多个 `Reducer`s 合并成一个 `Reducer`。
// 需要注意的是,`Reducer`s 的顺序很重要,`reducerA` 和 `reducerB` 组合与 `reducerB` 和 `reducerA` 组合是不一样的。
// 通常是把 Child Reducer 放在前面,Parent Reducer 放在后面。例如:
/// ```swift
/// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
///   // Combined before parent so that it can react to `.dismiss` while state is non-`nil`.
///   childReducer.optional().pullback(
///     state: \.child,
///     action: /ParentAction.child,
///     environment: { $0.child }
///   ),
///   // Combined after child so that it can `nil` out child state upon `.child(.dismiss)`.
///   Reducer { state, action, environment in
///     switch action
///     case .child(.dismiss):
///       state.child = nil
///       return .none
///     ...
///     }
///   },
/// )
/// ```
public static func combine(_ reducers: Self...) -> Self {
  .combine(reducers)
}

/// 上面那个方法的具体实现
public static func combine(_ reducers: [Self]) -> Self {
  Self { state, action, environment in
	  // 使用 `reduce` 方法把 `reducers` 合并成一个 `reducer`
    reducers.reduce(.none) { $0.merge(with: $1(&state, action, environment)) }
  }
}

// 自己与另外一个 Reducer 合并,先执行自己,后执行 `other`
public func combined(with other: Self) -> Self {
  Self { state, action, environment in
    self(&state, action, environment).merge(with: other(&state, action, environment))
  }
}

转换 Reducer

// 将 child reducer 转换为 parent reducer。通过提供 3 个转换来实现:
// - WritableKeyPath: 可以在 parent state 上获取/设置 child state。
// - CasePath(一个类似 `KeyPath` 的适用于 `enum` 的自定义类型): 可以在 parent action 上提取/嵌入 child action。
// - 闭包: 将 parent environment 转换为 child environment。

// 这个方法对于将一个大的 reducer 拆分成多个小的 reducers 非常重要。可以利用这个方法和 `combine` 方法实现这一需求。例如:
///    ```swift
///     // Parent domain that holds a child domain:
///     struct AppState { var settings: SettingsState, /* rest of state */ }
///     enum AppAction { case settings(SettingsAction), /* other actions */ }
///     struct AppEnvironment { var settings: SettingsEnvironment, /* rest of dependencies */ }
///
///     // A reducer that works on the child domain:
///     let settingsReducer = Reducer<SettingsState, SettingsAction, SettingsEnvironment> { ... }
///
///     // Pullback the settings reducer so that it works on all of the app domain:
///     let appReducer: Reducer<AppState, AppAction, AppEnvironment> = .combine(
///       settingsReducer.pullback(
///         state: \.settings,
///         action: /AppAction.settings,
///         environment: { $0.settings }
///       ),
///
///       /* other reducers */
///     )
///    ```
public func pullback<ParentState, ParentAction, ParentEnvironment>(
  state toChildState: WritableKeyPath<ParentState, State>,
  action toChildAction: CasePath<ParentAction, Action>,
  environment toChildEnvironment: @escaping (ParentEnvironment) -> Environment
) -> Reducer<ParentState, ParentAction, ParentEnvironment> {
  .init { parentState, parentAction, parentEnvironment in
	  // 从 parentAction 中提取 childAction
    guard let childAction = toChildAction.extract(from: parentAction) else { return .none }
    return self.reducer(
      &parentState[keyPath: toChildState], // 在 parent state 上获取/设置 child state
      childAction,
      toChildEnvironment(parentEnvironment)
    )
    .map(toChildAction.embed) // 将 child action 转换为 parent action
  }
}


// 上面那个方法的重载,唯一的区别是 `toChildState` 是一个 `CasePath`。
// 这个方法适用于 `AppState` 是 `enum` 的情况。
/// ```swift
/// // Parent domain that holds a child domain:
/// enum AppState { case loggedIn(LoggedInState), /* rest of state */ }
/// enum AppAction { case loggedIn(LoggedInAction), /* other actions */ }
/// struct AppEnvironment { var loggedIn: LoggedInEnvironment, /* rest of dependencies */ }
///
/// // A reducer that works on the child domain:
/// let loggedInReducer = Reducer<LoggedInState, LoggedInAction, LoggedInEnvironment> { ... }
///
/// // Pullback the logged-in reducer so that it works on all of the app domain:
/// let appReducer: Reducer<AppState, AppAction, AppEnvironment> = .combine(
///   loggedInReducer.pullback(
///     state: /AppState.loggedIn,
///     action: /AppAction.loggedIn,
///     environment: { $0.loggedIn }
///   ),
///
///   /* other reducers */
/// )
/// ```
public func pullback<ParentState, ParentAction, ParentEnvironment>(
  state toChildState: CasePath<ParentState, State>,
  action toChildAction: CasePath<ParentAction, Action>,
  environment toChildEnvironment: @escaping (ParentEnvironment) -> Environment,
  file: StaticString = #file,
  fileID: StaticString = #fileID,
  line: UInt = #line
) -> Reducer<ParentState, ParentAction, ParentEnvironment> {
  .init { parentState, parentAction, parentEnvironment in
    // 提取 childAction
    guard let childAction = toChildAction.extract(from: parentAction) else { return .none }
    // 提取 childState
    guard var childState = toChildState.extract(from: parentState) else {
      // warning 信息太长,省略
      runtimeWarning()
      return .none
    }
	  
    // 最后的 `self.run` 运行完成后,更新 parentState 的值
    defer { parentState = toChildState.embed(childState) }

    let effects = self.run(
      &childState,
      childAction,
      toChildEnvironment(parentEnvironment)
    )
    .map(toChildAction.embed) // 将 child action 转换为 parent action

    return effects
  }
}

// 通过仅在 state 不是 nil 时运行 non-optional reducer,
// 将在 non-optional state 下工作的 reducer 转换为在 optional state 下运行的reducer。
public func optional(
  file: StaticString = #file,
  fileID: StaticString = #fileID,
  line: UInt = #line
) -> Reducer<
  State?, Action, Environment
> {
  .init { state, action, environment in
    guard state != nil else {
		// warning 信息太长,省略
      runtimeWarning()
      return .none
    }
    return self.reducer(&state!, action, environment)
  }
}

// `pullback(state:action:environment:)` 的另一个版本,将可适用于一个元素的 reducer 转换为可适用于 `IdentifiedArrayOf` 数组的 reducer。例如:
/// ```swift
/// // Parent domain that holds a collection of child domains:
/// struct AppState { var todos: IdentifiedArrayOf<Todo> }
/// enum AppAction { case todo(id: Todo.ID, action: TodoAction) }
/// struct AppEnvironment { var mainQueue: AnySchedulerOf<DispatchQueue> }
///
/// // A reducer that works on an element's domain:
/// let todoReducer = Reducer<Todo, TodoAction, TodoEnvironment> { ... }
///
/// // Pullback the todo reducer so that it works on all of the app domain:
/// let appReducer = Reducer<AppState, AppAction, AppEnvironment>.combine(
///   todoReducer.forEach(
///     state: \.todos,
///     action: /AppAction.todo(id:action:),
///     environment: { _ in TodoEnvironment() }
///   ),
///   Reducer { state, action, environment in
///     ...
///   }
/// )
/// ```
public func forEach<ParentState, ParentAction, ParentEnvironment, ID>(
  state toElementsState: WritableKeyPath<ParentState, IdentifiedArray<ID, State>>,
  action toElementAction: CasePath<ParentAction, (ID, Action)>,
  environment toElementEnvironment: @escaping (ParentEnvironment) -> Environment,
  file: StaticString = #file,
  fileID: StaticString = #fileID,
  line: UInt = #line
) -> Reducer<ParentState, ParentAction, ParentEnvironment> {
  .init { parentState, parentAction, parentEnvironment in
    // 提取元素的 id 和 action
    guard let (id, action) = toElementAction.extract(from: parentAction)
    else { return .none }

    if parentState[keyPath: toElementsState][id: id] == nil {
      // 找不到 id 对应的元素,直接返回 `.none`
      // warning 信息太长,省略
      runtimeWarning()
      return .none
    }
    return
      self
      .reducer(
        &parentState[keyPath: toElementsState][id: id]!, // id 对应的元素
        action,
        toElementEnvironment(parentEnvironment)
      )
      .map { toElementAction.embed((id, $0)) }  // 将 child action 转换为 parent action
  }
}

// `pullback(state:action:environment:)` 的另一个版本,将可适用于一个元素的 reducer 转换为可适用于 `Dictionary` 的 reducer。
public func forEach<ParentState, ParentAction, ParentEnvironment, Key>(
  state toDictionaryState: WritableKeyPath<ParentState, [Key: State]>,
  action toKeyedAction: CasePath<ParentAction, (Key, Action)>,
  environment toValueEnvironment: @escaping (ParentEnvironment) -> Environment,
  file: StaticString = #file,
  fileID: StaticString = #fileID,
  line: UInt = #line
) -> Reducer<ParentState, ParentAction, ParentEnvironment> {
  .init { parentState, parentAction, parentEnvironment in
    // 提取元素的 key 和 action
    guard let (key, action) = toKeyedAction.extract(from: parentAction) else { return .none }

    if parentState[keyPath: toDictionaryState][key] == nil {
      // 找不到 key 对应的元素,直接返回 `.none`
      // warning 信息太长,省略
      runtimeWarning()
      return .none
    }
    return self.reducer(
      &parentState[keyPath: toDictionaryState][key]!, // key 对应的元素
      action,
      toValueEnvironment(parentEnvironment)
    )
    .map { toKeyedAction.embed((key, $0)) }  // 将 child action 转换为 parent action
  }
}

运行 Reducer

// 运行 reducer
public func run(
  _ state: inout State,
  _ action: Action,
  _ environment: Environment
) -> Effect<Action, Never> {
  self.reducer(&state, action, environment)
}

// 运行 reducer。
// `callAsFunction` 在 `【The Composable Architecture (TCA) 源码解析】02 - Effect` 这篇文章有详细讲解
public func callAsFunction(
  _ state: inout State,
  _ action: Action,
  _ environment: Environment
) -> Effect<Action, Never> {
  self.reducer(&state, action, environment)
}