此源码解析适用于
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)
}