一开始我其实只是想用 KVO 让 View Model 更响应式一些,可是 KVO 只支持 NSObject,用起来绑手绑脚,感觉没法进行下去。以前看过一些响应式的代码,想要模仿一下它们的风格。感觉要是有这么两个类型就好了:
Observable<T>
,它内部持有一个变量,当这个变量发生改变时,通知其观察者。
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
。同样的道理,我们也可以将 UIControl
的 addTarget
或者 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)) } } }
在初始化的过程中,observationBlock
会被调用并生成一个 Disposable
,一旦 KVO 触发,Observation
就会接收到一个新值事件,而这个事件则会继续传递给 Observation
的观察者们。Disposable
有 Observation
负责管理,这样子一旦 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) }
这样一来就清晰多了。
首先要做的是给 Emitter
和 Observable
加上一个产生 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 了。