【The Composable Architecture (TCA) 源码解析】06 - Effect 扩展之 Publisher

282 阅读2分钟

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

这个扩展主要是实现了 Publisher 协议,并且提供了一系列使用 Publisher 创建 Effect 的方法。

实现 Publisher 协议

extension Effect: Publisher {
  public typealias Output = Action

	// `Publisher` 协议唯一要求实现的方法
  public func receive<S: Combine.Subscriber>(
    subscriber: S
  ) where S.Input == Action, S.Failure == Failure {
	  // 把 `subscriber` 附着在自己的 `publisher` 上,
    // `subscriber` 就可以开始接收 `publisher` 发出的值。
    self.publisher.subscribe(subscriber)
  }

	// `Effect` 内部真正产生 `Action` 流的 `Publisher`
  var publisher: AnyPublisher<Action, Failure> {
    switch self.operation {
    case .none:
		// 不做任何操作,返回 `Empty`
      return Empty().eraseToAnyPublisher()
    case let .publisher(publisher):
		// `operation` 已经是 `.publisher` 类型,直接返回
      return publisher
    case let .run(priority, operation):
		// 异步任务类型的 `Effect` 转换成 `Publisher`。
		// 使用了自定义的 `Publishers.Create`,具体查看源码 `Internal/Create.swift`。
		// `Publishers.Create` 要求传入一个闭包,参数是 `Effect.Subscriber`,返回值是 `Cancellable`。
      return .create { subscriber in
		  // 在闭包里面,使用 `Task` 来执行 `operation`
        let task = Task(priority: priority) { @MainActor in
			// 在 `operation(send)` 运行完成后,发出 `.finished`
          defer { subscriber.send(completion: .finished) }
			// 定义一个 `Send`,将 `Action` 发给 `subscriber`
          let send = Send { subscriber.send($0) }
          await operation(send)
        }
			// 返回 `AnyCancellable`,目的是下游取消订阅的时候,可以取消 `task` 运行
        return AnyCancellable {
          task.cancel()
        }
      }
    }
  }
}

使用 Publisher 创建 Effect 方法

extension Effect {
  // 直接使用 `Publisher` 创建 `Effect`
  public init<P: Publisher>(_ publisher: P) where P.Output == Output, P.Failure == Failure {
    self.operation = .publisher(publisher.eraseToAnyPublisher())
  }

  // 使用 `value` 创建 `Effect`,并且马上把传入的 `value` 发出
  public init(value: Action) {
    self.init(Just(value).setFailureType(to: Failure.self))
  }

  // 使用 `error` 创建 `Effect`,并且马上把传入的 `error` 发出
  public init(error: Failure) {
    // 这里最好是使用 `Fail`,但在 iOS 13 有 bug(iOS 14 已修复),为了支持 iOS 13,这里做了一点变通。
    self.init(
      Deferred {
        Future { $0(.failure(error)) }
      }
    )
  }

	// 创建可以在将来异步提供单个值的 `Effect`。例如:
  /// ```swift
  /// Effect<Int, Never>.future { callback in
  ///   DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
  ///     callback(.success(42))
  ///   }
  /// }
  /// ```
  public static func future(
    _ attemptToFulfill: @escaping (@escaping (Result<Action, Failure>) -> Void) -> Void
  ) -> Self {
	  // 在创建 `Future` 时,传入的闭包会马上执行,所以这里使用了 `Deferred`,
	  // 等有订阅者订阅时才去执行闭包。
    Deferred { Future(attemptToFulfill) }.eraseToEffect()
  }

	// 创建可以在将来同步提供单个值的 `Effect`,与上面的异步很类似。例如:
  /// ```swift
  /// Effect<User, Error>.result {
  ///   let fileUrl = URL(
  ///     fileURLWithPath: NSSearchPathForDirectoriesInDomains(
  ///       .documentDirectory, .userDomainMask, true
  ///     )[0]
  ///   )
  ///   .appendingPathComponent("user.json")
  ///
  ///   let result = Result<User, Error> {
  ///     let data = try Data(contentsOf: fileUrl)
  ///     return try JSONDecoder().decode(User.self, from: $0)
  ///   }
  ///
  ///   return result
  /// }
  /// ```
  public static func result(_ attemptToFulfill: @escaping () -> Result<Action, Failure>) -> Self {
    Deferred { Future { $0(attemptToFulfill()) } }.eraseToEffect()
  }

	// 用闭包创建可以发送任意数量值的 `Effect`,并且可以发送 completion。例如:
  /// ```swift
  /// Effect.run { subscriber in
  ///   subscriber.send(MPMediaLibrary.authorizationStatus())
  ///
  ///   guard MPMediaLibrary.authorizationStatus() == .notDetermined else {
  ///     subscriber.send(completion: .finished)
  ///     return AnyCancellable {}
  ///   }
  ///
  ///   MPMediaLibrary.requestAuthorization { status in
  ///     subscriber.send(status)
  ///     subscriber.send(completion: .finished)
  ///   }
  ///   return AnyCancellable {
  ///     // Typically clean up resources that were created here, but this effect doesn't
  ///     // have any.
  ///   }
  /// }
  /// ```
  public static func run(
    _ work: @escaping (Effect.Subscriber) -> Cancellable
  ) -> Self {
    AnyPublisher.create(work).eraseToEffect()
  }

	// 创建执行某些工作的 `Effect`,并且不需要把数据反馈给 Store。如果抛出错误,`Effect` 将完成,错误将忽略。
  public static func fireAndForget(_ work: @escaping () throws -> Void) -> Self {
    Deferred { () -> Publishers.CompactMap<Result<Action?, Failure>.Publisher, Action> in
      try? work()
    	// 这里最好是返回 `Empty(completeImmediately: true)`。
    	// 但在 iOS 13.2 有 bug,它不会 complete,在 iOS 13.3 已修复。
		// 为了支持更低版本,这里做了一点变通。
      return Just<Action?>(nil)
        .setFailureType(to: Failure.self)
        .compactMap { $0 }
    }
    .eraseToEffect()
  }
}

extension Effect where Failure == Error {
	// 创建可以延迟执行某些工作并且可以同步发送数据给 Store 的 `Effect`。例如:
  /// ```swift
  /// Effect<User, Error>.catching {
  ///   let fileUrl = URL(
  ///     fileURLWithPath: NSSearchPathForDirectoriesInDomains(
  ///       .documentDirectory, .userDomainMask, true
  ///     )[0]
  ///   )
  ///   .appendingPathComponent("user.json")
  ///
  ///   let data = try Data(contentsOf: fileUrl)
  ///   return try JSONDecoder().decode(User.self, from: $0)
  /// }
  /// ```
  public static func catching(_ work: @escaping () throws -> Action) -> Self {
    .future { $0(Result { try work() }) }
  }
}

extension Publisher {
	// 把 `Publisher` 转换成 `Effect`
  public func eraseToEffect() -> Effect<Output, Failure> {
    Effect(self)
  }

	// 把 `Publisher` 转换成 `Effect`,同时执行转换
  public func eraseToEffect<T>(
    _ transform: @escaping (Output) -> T
  ) -> Effect<T, Failure> {
    self.map(transform)
      .eraseToEffect()
  }

	// 把 `Publisher` 转换成 `Effect`,把 Output 和 Failure 转换成 `Result`
  public func catchToEffect() -> Effect<Result<Output, Failure>, Never> {
    self.map(Result.success)
      .catch { Just(.failure($0)) }
      .eraseToEffect()
  }

	// 把 `Publisher` 转换成 `Effect`,把 Output(使用闭包转换) 和 Failure 转换成 `Result`
  public func catchToEffect<T>(
    _ transform: @escaping (Result<Output, Failure>) -> T
  ) -> Effect<T, Never> {
    self
      .map { transform(.success($0)) }
      .catch { Just(transform(.failure($0))) }
      .eraseToEffect()
  }

	// 把任意类型 Output 和 Failure 的 `Publisher` 转换成 `Effect`,并且忽略 Output 和 Failure。
  // 适用于想要执行一些操作,但不需要反馈任何数据给 Store。例如:
  /// ```swift
  /// case .buttonTapped:
  ///   return analyticsClient.track("Button Tapped")
  ///     .fireAndForget()
  /// ```
  public func fireAndForget<NewOutput, NewFailure>(
    outputType: NewOutput.Type = NewOutput.self,
    failureType: NewFailure.Type = NewFailure.self
  ) -> Effect<NewOutput, NewFailure> {
    return
      self
      .flatMap { _ in Empty<NewOutput, Failure>() }
      .catch { _ in Empty() }
      .eraseToEffect()
  }
}