单位(Units)
这篇文章讲了什么是RxSwift中的单位(units),为什么这是一个很重要的概念,怎么使用它们,怎么创建它们。
为什么会有Units
Swift有一个强大的类型系统可以用来增强应用的可靠性,并且让使用Rx变得更加直观方便。
Units这个概念是针对 RxCocoa 项目产生的。但是其中的原理也可以很容易运用到其他Rx项目的实现中。
Units不是一定要用的,你完全可以在你的项目中用普通的observable序列,所有RxCocoa APIs都支持普通的observable序列。
Units也可以用在不同模块的接口,来沟通和保护observable序列的属性。
当我们在写Cocoa/UIKit应用的时候,有一些属性是很重要的。
- 不把错误传递出来
- 在主线程观察
- 在主线程订阅
- 分享副作用
工作原理
Units的核心就是就是一个结构体,里面有一个observable序列的引用。
你可以把它们想成是一种observable序列的builder pattern。当我们创建一个序列的时候,调用 .asObservable()
可以一个unit变化成一个普通的observable序列。
为什么叫做Units
类比的方法很有助于帮我们去理解那些不熟悉的概念。对于Units的概念,我们可以用物理中单位(Units)的概念去类比RxCocoa中单位(Units)的概念。
类比:
物理 units | Rx units |
---|---|
数字 (一个数值) | observable序列 (一串值) |
钢量单位 (m, s, m/s, N ...) | Swift结构体 (Driver, ControlProperty, ControlEvent, ...) |
物理单位是一个数字加上一个相应的钢量单位。
Rx单位是一个observable序列加上一个相对应结构体,这个结构体描述了observable序列的属性。
在物理单位中,数字是最基本的元素,它们通常是实数或者负数。
在Rx单位中,Observable序列是最基本的组成部分。
物理单位和钢量分析会在复杂的计算时简化计算过程,同时减少错误的可能性
类型检查Rx units也会在写reactive程序的时候减少逻辑错误的可能性。
数字有运算符 +
, -
, *
, /
.
Observable序列也有操作符: map
, filter
, flatMap
...
物理单位运算通过相应的数字运算定义,例如:
对于物理单位的/
运算就是对于数字的/
运算。
11 m / 0.5 s = ...
- 首先,把单位转化成数字,使用运算符
/
11 / 0.5 = 22
- 然后,计算单位(m / s)
- 最后,合并结果 = 22 m / s
Rx units运算通过对observable序列的运算来定义(事实上这也是运算符工作的内部机制),例如:
对于Driver
的map
运算就是对于observable序列的map
运算。
let d: Driver<Int> = Driver.just(11)
driver.map { $0 / 0.5 } = ...
- 首先,把
Driver
转化成observable序列,然后应用map
运算符
let mapped = driver.asObservable().map { $0 / 0.5 } // 这个`map`是observable序列的运算符
- 然后合并它们得到答案
let result = Driver(mapped)
物理中有很多基本单位(m
, kg
, s
, A
, K
, cd
, mol
),它们是正交的
在RxCocoa
中有一些基本的observable序列属性,它们也是正交的。
* 不把错误传递出来
* 在主线程观察
* 在主线程订阅
* 共享副作用
物理学中,通过运算得到的单位由他们自己的名字
例如:
N (Newton) = kg * m / s / s
C (Coulomb) = A * s
T (Tesla) = kg / A / s / s
Rx的派生单位也有特殊的名字
例如
Driver = (不把错误传递出来) * (在主线程观察) * (共享副作用)
ControlProperty = (共享副作用) * (在主线程订阅)
不同物理单位之间的转化可以使用数字运算符*
, /
.
不同Rx单位之间的转化可以使用observable序列运算符
例如:
不把错误传递出来 = catchError
在主线程观察 = observeOn(MainScheduler.instance)
在主线程订阅 = subscribeOn(MainScheduler.instance)
共享副作用 = share* (one of the `share` operators)
RxCocoa units
Driver unit
- 不把错误传递出来
- 在主线程观察
- 共享副作用 (
shareReplayLatestWhileConnected
)
ControlProperty / ControlEvent
- 不把错误传递出来
- 在主线程订阅
- 在主线程观察
- 共享副作用
Driver
这是最精妙的一个单位。它的目的是让用reactive代码写UI层时更加简单方便。
为什么这个单位叫Driver
Driver的目的是模拟序列来驱动你的应用。
例如:
- 通过CoreData的数据来驱动UI
- 通过别的UI元素的值来驱动UI(bindings)
...
就像操作系统中的驱动一样,如果一个序列出错了,那么你的应用将会停止对用户输入的反馈。
很重要的一点是,这些元素必须在主线上被观察,因为UI元素和应用里的逻辑通常不是线程安全的。
另外,Driver
unit 需要创建一个能共享副作用的observable序列。
例如:
使用实例
下面这段代码是一段典型的初学者的代码:
let results = query.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
}
results
.map { "\($0.count)" }
.bindTo(resultCount.rx.text)
.addDisposableTo(disposeBag)
results
.bindTo(resultsTableView.rx.itemsWithCellIdentifier("Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.addDisposableTo(disposeBag)
这段代码做了以下几件事情:
- 调节对用户输入的反馈频率
- 向服务器发送请求,得到一组用户数据
- 得到的数据结果绑定到两个UI元素,数据列表tableView和一个现实结果数量的label
这段代码的问题在哪里?:
- 如果
fetchAutoCompleteItems
observable序列发生错误, (连接错误或者解析错误),这个错误将会让所有元素都断开绑定,让UI不再对新的请求做出反应。 - 如果
fetchAutoCompleteItems
在其他线程中返回结果,这个结果将会从其他线程绑定UI元素,这会导致程序会有不确定的崩溃的可能。 - 结果绑定了两个UI元素,这意味着对每次用户的请求,都需要做两次HTTP请求,每个UI元素一次,显然这并不是我们想要的。
一个更加完整稳定的版本应该像这样:
let results = query.rx.text
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.observeOn(MainScheduler.instance) // results are returned on MainScheduler
.catchErrorJustReturn([]) // in the worst case, errors are handled
}
.shareReplay(1) // HTTP requests are shared and results replayed
// to all UI elements
results
.map { "\($0.count)" }
.bindTo(resultCount.rx.text)
.addDisposableTo(disposeBag)
results
.bindTo(resultTableView.rx.itemsWithCellIdentifier("Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.addDisposableTo(disposeBag)
在一个很大的系统里像这样满足每一个要求去写出稳定的程序是很有挑战的,我们要介绍一个更简单地方法,那就是Units。
下面这段代码可以实现与上面的代码相同的功能:
let results = query.rx.text.asDriver() // This converts a normal sequence into a `Driver` sequence.
.throttle(0.3, scheduler: MainScheduler.instance)
.flatMapLatest { query in
fetchAutoCompleteItems(query)
.asDriver(onErrorJustReturn: []) // Builder just needs info about what to return in case of error.
}
results
.map { "\($0.count)" }
.drive(resultCount.rx.text) // If there is a `drive` method available instead of `bindTo`,
.addDisposableTo(disposeBag) // that means that the compiler has proven that all properties
// are satisfied.
results
.drive(resultTableView.rx.itemsWithCellIdentifier("Cell")) { (_, result, cell) in
cell.textLabel?.text = "\(result)"
}
.addDisposableTo(disposeBag)
这段代码做了什么?
首先asDriver
方法把ControlProperty
unit转化成一个Driver
unit。
query.rx.text.asDriver()
在这里不需要做什么额外的处理,Driver
有所有ControlProperty
unit的属性,而且增加了额外的属性,整个observable序列被封装成Driver
,仅仅是这样而已。
第二个改变是:
.asDriver(onErrorJustReturn: [])
任何一个observable序列只要有三个属性,就可以转化成Driver
unit
- 不传递出错误
- 在主线程观察
- 共享副作用 (
shareReplayLatestWhileConnected
)
你如果满足这些属性呢?用Rx运算符asDriver(onErrorJustReturn: [])
可以等于下面这段代码:
let safeSequence = xs
.observeOn(MainScheduler.instance) // observe events on main scheduler
.catchErrorJustReturn(onErrorJustReturn) // can't error out
.shareReplayLatestWhileConnected // side effects sharing
return Driver(raw: safeSequence) // wrap it up
最后一步是用drive
代替bindTo
drive
是Driver
unit定义的方法,这意味着如果你在代码中看见了drive
,那就意味着那个observable序列不会传递出错误,在主线程被观察,可以安全地绑定UI元素
理论上来讲,可以对ObservableType
或者其他接口调用drive
方法,所以为了更加安全,你可以在绑定UI元素之前,创建一个暂时的定义let results: Driver<[Results]> = ...
,我们让读者自己去判断需不需要这样子做。