【The Composable Architecture (TCA) 源码解析】02 - Effect

492 阅读5分钟

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

Effect 类型封装了可以在外部运行的工作单元,并且可以将 Action 反馈到 Store。这是一个用来做副作用的地方,如网络请求等。

Effect 是从 Reducer 返回的,所以 Store 可以在 Reducer 处理完毕后执行 Effect

有两种不同的方法来创建 Effect

  • 使用 Swift 自带的并发。根据不同需求,有三种方式创建 Effect

    • task(priority:operation:catch:file:fileID:line:):想要返回一个 Action
    • run(priority:operation:catch:file:fileID:line:):想要返回任意数量的 Action
    • fireAndForget(priority:_:):不返回任何 Action
  • 使用 Combine 框架。利用 Combine 的特性实现需求,最终利用 eraseToEffect或者 catchToEffectPublisher 转换为 Effect注意:这种方式是不推荐的,所以最终你应该使用 Swift 自带的并发来创建 Effect

重要Store 不是线程安全的,因此所有 Effect 必须在同一个线程上接收值,通常是主线程。这是使用 Combine 框架仅有的一个问题。如果使用 Swift 并发和 Effect 的三种方法 .task.run.fireAndForget,则线程问题由 Swift 自动处理。

Effect 的初始化

初始化 Effect 需要传入一个 Operation。也就是要告诉这个副作用需要做什么操作。每一种操作的解析如下:

  • .none:不需要做任何操作。
  • .publisher(AnyPublisher<Action, Failure>):操作类型是 Combine 框架的 AnyPublisher
  • .run(TaskPriority? = nil, @Sendable (Send<Action>) async -> Void):操作类型是执行 Concurrency 框架的 Task
public struct Effect<Action, Failure: Error> {
  @usableFromInline
  enum Operation {
    case none
    case publisher(AnyPublisher<Action, Failure>)
    case run(TaskPriority? = nil, @Sendable (Send<Action>) async -> Void)
  }

  @usableFromInline
  let operation: Operation

  @usableFromInline
  init(operation: Operation) {
    self.operation = operation
  }
}

Send 类型

用于把 Action 发送给系统,类似于 Redux 中的 Dispatch

@MainActor
public struct Send<Action> {
  public let send: @MainActor (Action) -> Void

  public init(send: @escaping @MainActor (Action) -> Void) {
    self.send = send
  }

  public func callAsFunction(_ action: Action) {
    guard !Task.isCancelled else { return }
    self.send(action)
  }

  public func callAsFunction(_ action: Action, animation: Animation?) {
    guard !Task.isCancelled else { return }
    withAnimation(animation) {
      self(action)
    }
  }
}
  • @MainActor:它来自于 Concurrency 框架,是一个全局 actor。被它标记的类型、变量等在运行过程中的相关操作,都会在主线程中执行。

  • callAsFunction:一个类型实现了这个方法后,可以直接对这个类型的实例进行调用。例如:

send(.started, animation: .spring())

想要详细了解,可以看文档 callAsFunction

创建 Effect

  • none: 不触发任何副作用。适用于必须返回一个 Effect,但不需要做任何副作用的时候。
public static var none: Self {
  Self(operation: .none)
}
  • task: 把只能处理一个 Action 的异步任务包装在 Effect 中。
public static func task(
  priority: TaskPriority? = nil,
  operation: @escaping @Sendable () async throws -> Action,
  catch handler: (@Sendable (Error) async -> Action)? = nil,
  file: StaticString = #file,
  fileID: StaticString = #fileID,
  line: UInt = #line
) -> Self {
  Self(
    operation: .run(priority) { send in
      do {
	// `operation()` 会返回一个 `Action`,
        // send 会把它发送给系统处理
        try await send(operation())
      } catch is CancellationError {
        // 忽略 `CancellationError`
        return
      } catch {
        guard let handler = handler else {
          #if DEBUG
            // 省略 runtimeWarning
          #endif
          return
        }
	// 跟 `do` 代码块里面的 `send(operation())` 类似,
        // 不同的是此处进行错误处理
        await send(handler(error))
      }
    }
  )
}
  • run: 把可以处理任意数量 Action 的异步任务包装在 Effect 中。
public static func run(
  priority: TaskPriority? = nil,
  operation: @escaping @Sendable (Send<Action>) async throws -> Void,
  catch handler: (@Sendable (Error, Send<Action>) async -> Void)? = nil,
  file: StaticString = #file,
  fileID: StaticString = #fileID,
  line: UInt = #line
) -> Self {
  Self(
    operation: .run(priority) { send in
      do {
        // 把 `send` 传给 `operation` 闭包,
        // 这样它内部就可以发送任意数量的 `Action` 给系统
        try await operation(send)
      } catch is CancellationError {
        // 忽略 `CancellationError`
        return
      } catch {
        guard let handler = handler else {
          #if DEBUG
          // 省略 runtimeWarning
          #endif
          return
        }
	// 跟 `do` 代码块里面的 `operation(send)` 类似,
        // 不同的是此处进行错误处理
        await handler(error, send)
      }
    }
  )
}
  • fireAndForget: 把不需要发送 Action 给系统的任务包装在 Effect 中。
public static func fireAndForget(
  priority: TaskPriority? = nil,
  _ work: @escaping @Sendable () async throws -> Void
) -> Self {
  // 内部直接调用了 `run` 方法
  Self.run(priority: priority) { _ in try? await work() }
}

合并 Effect

// 把一组任意数量的 `effects` 合并成一个 `Effect`,并同时执行 `effects`
@inlinable
public static func merge(_ effects: Self...) -> Self {
    Self.merge(effects)
}

// 把一个序列的 `effects` 合并成一个 `Effect`,并同时执行 `effects`
@inlinable
public static func merge<S: Sequence>(_ effects: S) -> Self where S.Element == Self {
    effects.reduce(.none) { $0.merge(with: $1) }
}

// 把自己与另外一个 `Effect` 合并成一个新的 `Effect`,并同时执行
@inlinable
public func merge(with other: Self) -> Self {
    switch (self.operation, other.operation) {
    case (_, .none):
        // 另外一个为 `none`,直接返回自己
        return self
    case (.none, _):
        // 自己为 `none`,返回 `other`
        return other
    case (.publisher, .publisher), (.run, .publisher), (.publisher, .run):
        // 只要至少其中一个为 `publisher`,就返回合并后的 `publisher`。
        // 因为 `Effect` 实现了 `Publisher` 协议,
        // 所以直接调用自带的 `Publishers.Merge` 进行合并
        return Self(operation: .publisher(Publishers.Merge(self, other).eraseToAnyPublisher()))16:20
    case let (.run(lhsPriority, lhsOperation), .run(rhsPriority, rhsOperation)):
        /// 两个都是 `run`,返回合并后的 `run`
        return Self(
            operation: .run { send in
                // `withTaskGroup` 是 `Combine` 自带的方法,
                // 把两个任务放到任务组中
                await withTaskGroup(of: Void.self) { group in
                    group.addTask(priority: lhsPriority) {
                        await lhsOperation(send)
                    }
                    group.addTask(priority: rhsPriority) {
                        await rhsOperation(send)
                    }
                }
            }
        )
    }
}

// 把一组任意数量的 `effects` 串联成一个 `Effect`,并逐个执行 `effects`
@inlinable
public static func concatenate(_ effects: Self...) -> Self {
    Self.concatenate(effects)
}

// 把一个集合的 `effects` 串联成一个 `Effect`,并逐个执行 `effects`
@inlinable
public static func concatenate<C: Collection>(_ effects: C) -> Self where C.Element == Self {
    effects.reduce(.none) { $0.concatenate(with: $1) }
}

// 把自己与另外一个 `Effect` 串联成一个新的 `Effect`,并逐个执行
@inlinable
@_disfavoredOverload
public func concatenate(with other: Self) -> Self {
    switch (self.operation, other.operation) {
    case (_, .none):
        // 另外一个为 `none`,直接返回自己
        return self
    case (.none, _):
        // 自己为 `none`,返回 `other`
        return other
    case (.publisher, .publisher), (.run, .publisher), (.publisher, .run):
        // 只要至少其中一个为 `publisher`,就返回串联后的 `publisher`。
        // 因为 `Effect` 实现了 `Publisher` 协议,
        // 所以直接调用自带的 `Publishers.Concatenate` 进行串联
        return Self(
            operation: .publisher(
                Publishers.Concatenate(prefix: self, suffix: other).eraseToAnyPublisher()
            )
        )
    case let (.run(lhsPriority, lhsOperation), .run(rhsPriority, rhsOperation)):
        /// 两个都是 `run`,返回合并后的 `run`
        return Self(
            operation: .run { send in
                // 因为是串联,所以先执行左边,再执行右边。
                // `cancellableValue` 是在 extension 中定义的 getter 类型的 async 属性,
                // 所以调用 `await task.cancellableValue` 就可以保证任务执行完成后,
                // 代码才会继续执行,这样就可以实现串联的效果
                if let lhsPriority = lhsPriority {
                    await Task(priority: lhsPriority) { await lhsOperation(send) }.cancellableValue
                } else {
                    await lhsOperation(send)
                }
                if let rhsPriority = rhsPriority {
                    await Task(priority: rhsPriority) { await rhsOperation(send) }.cancellableValue
                } else {
                    await rhsOperation(send)
                }
            }
        )
    }
}

/// 把泛型 `Action` 转换成其他类型
@inlinable
public func map<T>(_ transform: @escaping (Action) -> T) -> Effect<T, Failure> {
    switch self.operation {
    case .none:
        return .none
    case let .publisher(publisher):
        // 直接调用 `publisher.map` 转换
        return .init(operation: .publisher(publisher.map(transform).eraseToAnyPublisher()))
    case let .run(priority, operation):
        return .init(
            operation: .run(priority) { send in
                // 在传入的 `Send` 参数内部转换
                await operation(
                    Send { action in
                        send(transform(action))
                    }
                )
            }
        )
    }
}