一些 RxSwift 思考题 - 回答

这篇文章是对上一篇 一些 RxSwift 思考题 的解答,如果您对这篇解答有什么疑问或者错误的地方要指出来,欢迎直接在下面评论,我会及时的回复和修正。

思考这些问题可以帮助你更好的理解什么是订阅、Observable 有什么特性、如何比较优雅地写 Rx 代码。

# 取消一个请求

# 请求一个列表数据时,切换新的分类,取消上一次的网络请求(如果未完成)。该使用什么操作符?

使用 flatMapLatest 即可,不考虑上一个 flatMap 的 Observable ,每当有新的值过来时,都立即进行 flatMap 操作(并抛弃上一个 Observable)。

示例代码大概如下:

enum TopChart {
  case paid
  case free
  case topGrossing
}

let segmentedControl = UISegmentedControl()

segmentedControl
    .rx.selectedSegmentIndex.asDriver()
    .map { index in mapToTopChart(index) }
    .flatMapLatest { topChart in someRequest(topChart) }
# 单纯地发送一个请求,如何取消该网络请求?该使用什么操作符?

使用 takeUntil ,当 takeUntil 中的 Observable 发射一个时,结束对应的 Observable 。

let cancelbutton = UIbutton()

someRequest()
    .takeUntil(cancelbutton.rx.tap)

结合到实际情况(添加 flatMap 等操作后),代码大概如下:

segmentedControl
    .rx.selectedSegmentIndex.asDriver()
    .map { index in mapToTopChart(index) }
    .flatMapLatest { topChart in
      someRequest(topChart).takeUntil(cancelbutton.rx.tap.asDriver())
    }

# 确保获取最新的结果

# 在进行一些操作时,我们可能需要获取某些最新的值,比如一个下单页面,点击确定,总是应当带上订单最新的数据。该使用什么操作符?可以使用 combineLatest 吗?可以使用 withLatestFrom 吗?可以使用 flatMap 吗?
# 如果可以,他们有什么区别吗?

combineLatest 应该是不可以的,当我们 combine 了 button 的点击和下单的结果变动,此时每当有一个 Observable 发射一个值时,combineLatest 都会发射一个值。这应该不是我们想要的。我们期待的是,每当点击 button 后,才发射一个值。

从上图中我们可以看到在点击 button 后,currentValue 有变化时,result 中还是有输出。

使用 withLatestFrom 和 flatMap 都是可以的。先来看 withLatestFrom :

从上图中可以看出这应该就是我们所需要的了,没毛病。

再来看 flatMap :

可以看到使用 flatMap 也是可以的(我们是通过点击 button 时订阅 currentValue 完成的)。但你应该注意到了,我将 currentValue 从 PublishSubject 换成 ReplaySubject ,以及在 flatMap 操作后面添加了一个 take(1) 。

二者区别主要在于 withLatestFrom 和 flatMap 的订阅时机不同,withLatestFrom 是立即被订阅,并发射了个值 1 ,而 flatMap 是在点击 button 之后才被订阅。

由于 flatMap 的订阅相对较晚,如果使用 PublishSubject 则可能收不到当前的值,需要使用诸如 ReplaySubject 做一个值的缓存。为了忽略点击 button 后又修改了值的情况,我加入了 take(1) 只获取第一个数据。

不注意这些区别,在使用时,可能会出现一些不预期影响。比如使用 withLatestFrom 时,currentValue 立即被订阅,相关代码立即执行。假设 currentValue 是个网络请求,这个网络请求则会在点击 button 之前执行,这可能不是我们想要的。

此外两种使用方法还可以帮我们做表单填写验证,分别对应点击 button 前验证、和点击 button 后验证两种场景。

# 多个异步请求

# 依次发送两个请求,第二个请求需要第一个请求的参数和响应结果,该如何组织操作符?列举几种?

# 如何跟踪上述请求状态?该使用什么操作符?

需要使用 using 操作符(当然你也可以使用 do 之类可以监听 Observable 生命周期的操作符)。

以一个网络请求为例,URLSession.share.rx.request(someRequest),当它被订阅时发送网络请求,网络请求完成后,生命周期结束。网络请求的状态刚好的 Observable 的声明周期对应上。

RxExample 中有对 using 操作符使用的示例: https://github.com/ReactiveX/RxSwift/blob/master/RxExample/RxExample/Services/ActivityIndicator.swift。

上图是对两个连续网络请求使用的示例。

# 重复请求

# 点击一个按钮发送一个请求,当该请求完成前,该按钮点击都不应该继续发送请求。该使用什么操作符?

flatMapFirst。

# Cell 复用怎么办

# 遇到过 Cell 中使用 button 重复订阅问题吗?如何解决比较好?
# 解决复用后,将一个 button 点击绑定到一个 PublishSubject 可能会有什么问题?

为 Cell 添加一个 DisposeBag,在 prepareForReuse 时,更换新的 DisposeBag 。

var disposeBag = DisposeBag()

override func prepareForReuse() {
    super.prepareForReuse()
    disposeBag = DisposeBag()
}

使用是大概如下:

cell.button.rx.tap.subscribe().dispose(by: cell.disposeBag)

在这里用到一个 PublishSubject 可能会导致下一次重用时失效:

let publishSubject = PublishSubject<()>()
// ...

cell.button.rx.tap.bindTo(publishSubject).dispose(by: cell.disposeBag)

需要做如下修改:

cell.button.rx.tap
  .subscribe(onNext: {
    publishSubject.onNext(())
  })
  .dispose(by: cell.disposeBag)

# flatMap 同一个实例会怎么样

正常地订阅一次。

let value = Observable.just(1)

button.rx.tap.asObservable()
  .flatMap { value }

这里每次点击 button 都会收到一个 1。

# 创建一个 Observable ,每当订阅该 Observable 都将发送一个请求,每次请求带上一个的随机的 uuid

请配合 RxCocoa 提供的 URLSession 扩展方法完成。

使用 defer 是比较好的方式:

Observable.just 替换成 URLSession 即可。

# 检查输入结果是否和最初第一个值相同

创建一个操作符,可以检查输入结果是否和最初的一样。比如一个 TextField 最初是 text1 ,经过一顿乱输,如何判断最终输入结果是否和最初相同?请尽量复用该操作符到各个场景。

extension ObservableConvertibleType where E: Equatable {

    func isEqualOriginValue() -> Observable<(value: E, isEqualOriginValue: Bool)> {
        return self.asObservable()
            .scan(nil as (origin: E, current: E)?, accumulator: { acc, x -> (origin: E, current: E)? in
                if let acc = acc {
                    return (origin: acc.origin, current: x)
                } else {
                    return (origin: x, current: x)
                }
            })
            .map { diff -> (value: E, isEqualOriginValue: Bool) in
                return (diff!.current, isEqualOriginValue: diff!.origin == diff!.current)
        }
    }

}

# invokeMethod 和 sendMessage 的区别

sentMessage 和 methodInvoked 只有一个区别,sentMessage 会在调用方法前发送值, methodInvoked 会在调用方法后发送值。

# 如何重复执行某个操作?

比如,发送一个网络请求后,点击 button 可以重复发送一个该网络请求。创建一个操作符以复用。

extension ObservableConvertibleType {

    func repeatWhen<O: ObservableType>(_ other: O) -> Observable<E> {
        return other.map { _ in }
            .startWith(())
            .flatMap { () -> Observable<E> in
                self.asObservable()
        }
    }

}