【The Composable Architecture 源码解析】05 - Effect 扩展之 Debounce & Defer & Throttle

103 阅读2分钟

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

debounce

这个扩展的作用是将普通 Effect 变成防抖动的。

extension Effect {
    public func debounce<S: Scheduler>(
    id: AnyHashable,
    for dueTime: S.SchedulerTimeType.Stride,
    scheduler: S,
    options: S.SchedulerOptions? = nil
  ) -> Self {
    switch self.operation {
    case .none:
      return .none
    case .publisher, .run:
		// 利用 `Combine` 自带的 `delay` 方法和 上一篇文章讲解的 `cancellable` 实现防抖动。
		// 例如,假设防抖动的时间为 3 秒,每秒调用一次 `debounce` 方法:
		// 第 1 秒调用:`Just` 发出的值,因为使用了 `delay`,要 3 秒后才会真正发出;
		// 第 2 秒调用:因为使用 `cancellable`,第 1 秒产生的 `Just` 会无法发出值;
		// 第 3 秒调用:因为使用 `cancellable`,第 2 秒产生的 `Just` 会无法发出值;
		// 第 6 秒:距离第 3 秒调用已经过去了 3 秒,第 3 秒产生的 `Just` 正常发出值,,然后使用 `flatMap` 把 `Just` 的值转换为 `publisher` 的值。
		// 最终实现了防抖动。
      return Self(
        operation: .publisher(
          Just(())
            .setFailureType(to: Failure.self)
            .delay(for: dueTime, scheduler: scheduler, options: options)
            .flatMap { self.publisher.receive(on: scheduler) }
            .eraseToAnyPublisher()
        )
      )
      .cancellable(id: id, cancelInFlight: true)
    }
  }

  // 上面那个方法的重载方法,这里的 `id` 是一个 `Any.Type`
  public func debounce<S: Scheduler>(
    id: Any.Type,
    for dueTime: S.SchedulerTimeType.Stride,
    scheduler: S,
    options: S.SchedulerOptions? = nil
  ) -> Self {
    self.debounce(id: ObjectIdentifier(id), for: dueTime, scheduler: scheduler, options: options)
  }
}

deferred

这个扩展的作用是延迟 Effect 的执行。从代码可以看到,它的实现与 debounce 非常类似,直接利用 Combine 自带的 delay 方法实现。

extension Effect {
  public func deferred<S: Scheduler>(
    for dueTime: S.SchedulerTimeType.Stride,
    scheduler: S,
    options: S.SchedulerOptions? = nil
  ) -> Self {
    switch self.operation {
    case .none:
      return .none
    case .publisher, .run:
      return Self(
        operation: .publisher(
          Just(())
            .setFailureType(to: Failure.self)
            .delay(for: dueTime, scheduler: scheduler, options: options)
            .flatMap { self.publisher.receive(on: scheduler) }
            .eraseToAnyPublisher()
        )
      )
    }
  }
}

throttle

这个扩展的作用是将普通 Effect 变成限流的。

extension Effect {
  public func throttle<S: Scheduler>(
    id: AnyHashable,
    for interval: S.SchedulerTimeType.Stride,
    scheduler: S,
    latest: Bool // 是否发布最新的值。如果是 `false`,将发出在 `interval` 期间接收的第一个值
  ) -> Self {
    switch self.operation {
    case .none:
      return .none
    case .publisher, .run:
      return self.receive(on: scheduler) // 在指定的 `scheduler` 上接收值
        .flatMap { value -> AnyPublisher<Action, Failure> in
			// 加锁,保证相关数据的准确性
          throttleLock.lock()
          defer { throttleLock.unlock() }

          guard let throttleTime = throttleTimes[id] as! S.SchedulerTimeType? else {
// --------- 缓存中没有 `id` 对应的时间,说明是第一次接收到值,应该直接发出值 ---------
			  // 把 `id` 对应的时间设置为当前时间
            throttleTimes[id] = scheduler.now
			  // 把 `id` 对应的值设置为 `nil`
            throttleValues[id] = nil
			  // 发出值
            return Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher()
          }

// -------------------- 从第二次接收到值开始,代码就会执行到这里 --------------------
			
			// 根据 `latest` 得到下一次要发出的值:
			// 如果 `latest` 为 `true`,使用当前接收到的值 `value`,
			// 否则使用缓存的值(因为每次发出值后 `throttleValues[id]` 都会被重置为 `nil`, 所以
			// 最后的 ` ?? value` 就能保证是在 `interval` 期间接收的第一个值)
          let value = latest ? value : (throttleValues[id] as! Action? ?? value)
          throttleValues[id] = value

          guard throttleTime.distance(to: scheduler.now) < interval else {
// ------------- 缓存时间与当前时间的间隔已经超过 `interval`,应该发出值 -------------
			  // 把 `id` 对应的时间设置为当前时间
            throttleTimes[id] = scheduler.now
			  // 把 `id` 对应的值设置为 `nil`
            throttleValues[id] = nil
			  // 发出值
            return Just(value).setFailureType(to: Failure.self).eraseToAnyPublisher()
          }

// ---- 缓存时间与当前时间的间隔小于 `interval`,需要延迟到时间间隔等于 `interval` ----

          return Just(value)
			  // 使用 `delay` 延迟发出值
            .delay(
				// 计算延迟的时间:当前时间与 (缓存时间 + `interval`) 的间隔
              for: scheduler.now.distance(to: throttleTime.advanced(by: interval)),
              scheduler: scheduler
            )
            .handleEvents(
              receiveOutput: { _ in
                throttleLock.sync {
					// `delay` 发出了值,重置相关数据
                  throttleTimes[id] = scheduler.now
                  throttleValues[id] = nil
                }
              }
            )
            .setFailureType(to: Failure.self)
            .eraseToAnyPublisher()
        }
        .eraseToEffect()
		  // 与 `debounce` 类似,这里使用 `cancellable` 可以把 `interval` 期间产生的不应该发出值的 `Just` 取消掉
        .cancellable(id: id, cancelInFlight: true)
    }
  }

  // 上面那个方法的重载方法,这里的 `id` 是一个 `Any.Type`
  public func throttle<S: Scheduler>(
    id: Any.Type,
    for interval: S.SchedulerTimeType.Stride,
    scheduler: S,
    latest: Bool
  ) -> Self {
    self.throttle(id: ObjectIdentifier(id), for: interval, scheduler: scheduler, latest: latest)
  }
}

var throttleTimes: [AnyHashable: Any] = [:]
var throttleValues: [AnyHashable: Any] = [:]
// 递归锁,保证相关数据的准确性。
// 使用递归锁的原因是在 `Store.send(_:originatingFrom)` 方法中有递归操作。
let throttleLock = NSRecursiveLock()