一开始我其实只是想用 KVO 让 View Model 更响应式一些,可是 KVO 只支持 NSObject,用起来绑手绑脚,感觉没法进行下去。以前看过一些响应式的代码,想要模仿一下它们的风格。感觉要是有这么两个类型就好了:

  1. Observable<T>,它内部持有一个变量,当这个变量发生改变时,通知其观察者。
  2. Emitter<T>,当 ViewModel 发生了什么需要告知 ViewController 时,可以通过 Emitter 发送一个事件,通知其所有的观察者。

譬如说我们要写一个掷骰子的程序,这个程序最奇特的一处是当我们多次掷到的数字之和超过 36,就会弹出一个弹窗告诉我们掷到了 36。我们的 ViewModel 可以这样子写:

1
2
3
4
5
6
7
8
9
10
11
12
class ViewModel {
private var sum = 0
let num = Observable<Int>(1)
let thirtySix = Emitter<Void>(())

func roll() {
let newNum = Int.random(in: 1...6)
num.val = newNum
sum += newNum
if sum >= 36 { thirtySix.emit(()) }
}
}

如此一来 ViewController 就可以通过监听 num 现实当前掷到的数字和 thirtySix 来得知应该合适显示弹窗了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ViewController {
let viewModel = ViewModel()
let disposeBag = DisposeBag()

override func viewDidLoad() {
super.viewDidLoad()

viewModel.num.subscribe { [weak self] num in
self?.label.text = "\(num)"
}.disposed(by: disposeBag)

viewModel.thirtySix.subscribe { [weak self] in
self?.alert()
}.disposed(by: disposeBag)
}
}

实现

就如同 KVO 那样,我们监听之后,总需要有个什么东西持有这个 block,自然而然的,我们就需要这个东西能够在恰当的时候被释放,这样 block 就不会一直被执行了。像各种各样的响应式框架那样,这种东西应该被叫做 Disposable,它有一个 dispose 方法,能够消除监听效果。

说起 KVO,我们不妨用 Disposable 包装一下 NSKeyValueObservation

1
2
3
4
5
6
7
8
9
extension NSObjectProtocol where Self: NSObject {
func subscribe<V>(_ keyPath: KeyPath<Self, V>, onChange: @escaping (V, V?) -> Void) -> Disposable {
let observation = observe(keyPath, options: [.initial, .new]) { _, change in
guard let newValue = change.newValue else { return }
onChange(newValue, change.oldValue)
}
return Disposable { observation.invalidate() }
}
}

写着写着我们就大概清楚 Disposable 应该如何实现了。而且 Disposable 不仅具有取消监听的作用,它还持有了 NSKeyValueObservation 的强引用,也就是说只要我们持有了 Disposable,也就自然持有了 NSKeyValueObservation。同样的道理,我们也可以将 UIControladdTarget 或者 NotificationCenter 通过这种方式进行监听,这样我们就能通过 DisposeBag 统一管理了。

那属于 Observable<V>Emitter<V>NSKeyValueObservation 又是什么呢?在这里我们就再引入一个类吧。姑且叫它 Observation<V> 吧。

对于一个 Observation,它可以接受一个事件 Event,并将这个 Event 传递给 subscriber。事件的定义很简单,它既可以是一个新值,也可以是一次错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
enum Event<V> {
case next(V)
case failure(Error)
}

class Observation<V> {
public enum PossibleEvent {
case next
case failure
}

private var observers = [(String, (Event) -> Void)]()
private var latestEvent: Event<V>? = nil
private let disposeBag = DisposeBag()
deinit { disposeBag.dispose() }

func action(_ event: Event<V>) {
observers.forEach { $0.1(event) }
latestEvent = event
}

public func subscribeEvent(perform block: @escaping (Event) -> Void) -> Disposable {
let uuid = UUID().uuidString
observers.append((uuid, block))
if let e = latestEvent { block(e) }
return Disposable { [uuid] in self.observers.removeAll(where: { $0.0 == uuid }) }
}
}

通过 subscribeEvent 方法我们可以注册订阅 Observation 的事件,每次注册订阅之后我们将传入的 block 保存到 observers 当中,我们给每一个 block 一个 UUID,这样我们就能够在 dispose 的时候准确的删掉不需要的 block

其实更多的时候,我们才不在乎什么错误!所以可以加上这个么一个方法。

1
2
3
4
5
public func subscribe(onNext: @escaping (V) -> Void) -> Disposable {
return subscribeEvent { event in
if case let .next(v) = event { onNext(v) }
}
}

感觉不错。现在只剩下一个问题了,我们要如何创建一个 Observation<V> 呢?不妨再用 KVO 套一下。要生成一个监听 KVO 的 Observation<V>,我们就需要把 Observation<V> 订阅以及取消订阅 KVO 的逻辑教导给 Observation<V>。我们可以在初始化的时候进行这个步骤:

1
2
3
init(observeBlock: (Observation) -> Disposable) {
observeBlock(self).disposed(by: disposeBag)
}

这样我们就可以把 KVO 订阅写成这样了:

1
2
3
4
5
6
7
public func observe<V>(_ keyPath: KeyPath<Self, V>) -> Observation<V> {
return Observation { observation in
return self.subscribe(keyPath) { new, _ in
observation.action(.next(new))
} // 一个 Disposable { keyValueObservation.invalidate() }
}
}

在初始化的过程中,observationBlock 会被调用并生成一个 Disposable,一旦 KVO 触发,Observation 就会接收到一个新值事件,而这个事件则会继续传递给 Observation 的观察者们。DisposableObservation 负责管理,这样子一旦 Observation 被释放,对应的 KVO 订阅也会被释放。

但这里还有一个问题,在 observeBlock 中我们传入了 Observation 自身,而它在 block 内部又被 subscribe 的 block 给捕获了,如此产生的 Disposable 又返回给了 Observation 自己持有。铛铛铛,引用循环出现了。为了跳出这个圈,我们可以在 subscribe 的时候弱捕获 Observation,但这样很容易犯错,所以可以选择写一个 Wrapper 包住它。

1
2
3
4
5
6
7
8
class WeakObservation<V> {
weak var observation: Observation<V>?
...
}

init(observeBlock: (WeakObservation) -> Disposable) {
observeBlock(WeakObservation(self)).disposed(by: disposeBag)
}

这样一来就清晰多了。

首先要做的是给 EmitterObservable 加上一个产生 Observation 的方法。我们大可以参考 Observation 的实现方式,将所有对它们的订阅行为储存起来。它们在这个阶段大体相同,所以我给了他们一个爸爸 Notifier<V>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Notifier<V> {    
typealias ObserverBlock = (Observation<V>.Event) -> Void
typealias Observer = (String, ObserverBlock)

private var observers = [Observer]()
fileprivate init() {}

public var latestEvent: Observation<V>.Event? = nil

func notify(_ event: Observation<V>.Event) {
latestEvent = event
observers.forEach { $0.1(event) }
}

private func addObserver(_ block: @escaping ObserverBlock) -> Disposable {
let uuid = UUID().uuidString
observers.append((uuid, block))
return Disposable { [weak self, uuid] in
guard let self = self else { return }
self.observers.removeAll(where: { $0.0 == uuid })
}
}

public func observe() -> Observation<V> {
return Observation { observation in
let queue = DispatchQueue.main
return Observation { observation in
if let e = self.latestEvent {
queue.safeAsync { observation.action(e) }
}
return self.addObserver { event in
queue.safeAsync { observation.action(event) }
}
}
}
}
}

只要两个儿子在合适的时候调用 notify 方法就行了。至此我们的掷骰子程序就能用了!

操作符

但我们又怎么能就此满足,如果去看一些响应式的代码,会发现有很多将一个 Observation 转换为另一个 Observation 的代码……好吧,那我们也要有。

1
2
3
4
5
6
viewModel.num.observe()
.ignoreLatest()
.map(String.init)
.map { $0 + "!!!" }
.bind(to: textField, at: \.text)
.disposed(by: disposeBag)

幸亏目前的实现让我们能够很简单地实现这个功能,我们只需要在原有的 Observation 上创建新的 Observation 就可以了:

1
2
3
4
5
6
7
8
9
10
public func map<M>(_ transform: @escaping (V) -> M) -> Observation<M> {
return Observation<M> { observation in
return self.subscribeEvent { event in
switch event {
case let .next(v): observation.action(.next(transform(v)))
case let .failure(e): observation.action(.failure(e))
}
}
}
}

而更多类型的操作符都能通过同样的方式实现,就不再累述了。

UIControl

不知道有没有人和我一样不喜欢 UIControl.addTarget。要让 UIControl 支持上述的响应式,需要首先给它一个 subscribe 方法让它返回一个 Disposable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TargetAction: NSObject {
private let block: () -> Void

init(action: @escaping () -> Void) {
self.block = action
super.init()
}

@objc func performAction() {
block()
}
}

func subscribe(_ event: UIControl.Event, block: @escaping () -> Void) -> Disposable {
let targetAction = TargetAction(action: block)
addTarget(targetAction, action: #selector(TargetAction.performAction), for: event)
return Disposable { _ = targetAction }
}

我们需要一个叫做 TargetAction 的类充当中间人,它将会把我们传入的 block 存起来,并在一个标记为 @objc 的方法中调用,这样一来在这个方法当中我们就不需要像以前那样把真正的 subscriber 传进来了。有了这个方法之后,我们就能够创建 observe 方法。

1
2
3
4
5
func observe(_ event: UIControl.Event) -> Observation<Void> {
return Observation { observation in
return self.subscribe(event) { observation.action(.next(())) }
}
}

有一些 UIControl 的变量并不支持 KVO,比如说 UITextField.text,我们会需要监听它的 .editingChanged 来感知这个值的变化,为了方便使用,我们可以再增加一个方法返回某个 keyPath 的值。

1
2
3
textField.observe(.editingChanged, take: \.text)
.subscribe { viewModel.search($0) }
.disposed(by: disposeBag)

爽。

最后

最后我们只需要在 Podfile 里加入 RxSwift,然后把上面写的代码删掉,就能愉快地 Reactive Programming 了。