單位(Units)
這篇文章講了什么是RxSwift中的單位(units),為什么這是一個很重要的概念,怎么使用它們,怎么創(chuàng)建它們。
為什么會有Units
Swift有一個強(qiáng)大的類型系統(tǒng)可以用來增強(qiáng)應(yīng)用的可靠性,并且讓使用Rx變得更加直觀方便。
Units這個概念是針對 RxCocoa 項(xiàng)目產(chǎn)生的。但是其中的原理也可以很容易運(yùn)用到其他Rx項(xiàng)目的實(shí)現(xiàn)中。
Units不是一定要用的,你完全可以在你的項(xiàng)目中用普通的observable序列,所有RxCocoa APIs都支持普通的observable序列。
Units也可以用在不同模塊的接口,來溝通和保護(hù)observable序列的屬性。
當(dāng)我們在寫Cocoa/UIKit應(yīng)用的時候,有一些屬性是很重要的。
- 不把錯誤傳遞出來
- 在主線程觀察
- 在主線程訂閱
- 分享副作用
工作原理
Units的核心就是就是一個結(jié)構(gòu)體,里面有一個observable序列的引用。
你可以把它們想成是一種observable序列的builder pattern。當(dāng)我們創(chuàng)建一個序列的時候,調(diào)用 .asObservable() 可以一個unit變化成一個普通的observable序列。
為什么叫做Units
類比的方法很有助于幫我們?nèi)ダ斫饽切┎皇煜さ母拍?。對于Units的概念,我們可以用物理中單位(Units)的概念去類比RxCocoa中單位(Units)的概念。
類比:
| 物理 units | Rx units |
|---|---|
| 數(shù)字 (一個數(shù)值) | observable序列 (一串值) |
| 鋼量單位 (m, s, m/s, N ...) | Swift結(jié)構(gòu)體 (Driver, ControlProperty, ControlEvent, ...) |
物理單位是一個數(shù)字加上一個相應(yīng)的鋼量單位。
Rx單位是一個observable序列加上一個相對應(yīng)結(jié)構(gòu)體,這個結(jié)構(gòu)體描述了observable序列的屬性。
在物理單位中,數(shù)字是最基本的元素,它們通常是實(shí)數(shù)或者負(fù)數(shù)。
在Rx單位中,Observable序列是最基本的組成部分。
物理單位和鋼量分析會在復(fù)雜的計(jì)算時簡化計(jì)算過程,同時減少錯誤的可能性
類型檢查Rx units也會在寫reactive程序的時候減少邏輯錯誤的可能性。
數(shù)字有運(yùn)算符 +, -, *, /.
Observable序列也有操作符: map, filter, flatMap ...
物理單位運(yùn)算通過相應(yīng)的數(shù)字運(yùn)算定義,例如:
對于物理單位的/運(yùn)算就是對于數(shù)字的/運(yùn)算。
11 m / 0.5 s = ...
- 首先,把單位轉(zhuǎn)化成數(shù)字,使用運(yùn)算符
/11 / 0.5 = 22 - 然后,計(jì)算單位(m / s)
- 最后,合并結(jié)果 = 22 m / s
Rx units運(yùn)算通過對observable序列的運(yùn)算來定義(事實(shí)上這也是運(yùn)算符工作的內(nèi)部機(jī)制),例如:
對于Driver的map運(yùn)算就是對于observable序列的map運(yùn)算。
let d: Driver<Int> = Driver.just(11)
driver.map { $0 / 0.5 } = ...
- 首先,把
Driver轉(zhuǎn)化成observable序列,然后應(yīng)用map運(yùn)算符
let mapped = driver.asObservable().map { $0 / 0.5 } // 這個`map`是observable序列的運(yùn)算符
- 然后合并它們得到答案
let result = Driver(mapped)
物理中有很多基本單位(m, kg, s, A, K, cd, mol),它們是正交的
在RxCocoa中有一些基本的observable序列屬性,它們也是正交的。
* 不把錯誤傳遞出來
* 在主線程觀察
* 在主線程訂閱
* 共享副作用
物理學(xué)中,通過運(yùn)算得到的單位由他們自己的名字
例如:
N (Newton) = kg * m / s / s
C (Coulomb) = A * s
T (Tesla) = kg / A / s / s
Rx的派生單位也有特殊的名字
例如
Driver = (不把錯誤傳遞出來) * (在主線程觀察) * (共享副作用)
ControlProperty = (共享副作用) * (在主線程訂閱)
不同物理單位之間的轉(zhuǎn)化可以使用數(shù)字運(yùn)算符*, /.
不同Rx單位之間的轉(zhuǎn)化可以使用observable序列運(yùn)算符
例如:
不把錯誤傳遞出來 = 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的目的是模擬序列來驅(qū)動你的應(yīng)用。
例如:
- 通過CoreData的數(shù)據(jù)來驅(qū)動UI
- 通過別的UI元素的值來驅(qū)動UI(bindings)
...
就像操作系統(tǒng)中的驅(qū)動一樣,如果一個序列出錯了,那么你的應(yīng)用將會停止對用戶輸入的反饋。
很重要的一點(diǎn)是,這些元素必須在主線上被觀察,因?yàn)閁I元素和應(yīng)用里的邏輯通常不是線程安全的。
另外,Driver unit 需要創(chuàng)建一個能共享副作用的observable序列。
例如:
使用實(shí)例
下面這段代碼是一段典型的初學(xué)者的代碼:
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)
這段代碼做了以下幾件事情:
- 調(diào)節(jié)對用戶輸入的反饋頻率
- 向服務(wù)器發(fā)送請求,得到一組用戶數(shù)據(jù)
- 得到的數(shù)據(jù)結(jié)果綁定到兩個UI元素,數(shù)據(jù)列表tableView和一個現(xiàn)實(shí)結(jié)果數(shù)量的label
這段代碼的問題在哪里?:
- 如果
fetchAutoCompleteItemsobservable序列發(fā)生錯誤, (連接錯誤或者解析錯誤),這個錯誤將會讓所有元素都斷開綁定,讓UI不再對新的請求做出反應(yīng)。 - 如果
fetchAutoCompleteItems在其他線程中返回結(jié)果,這個結(jié)果將會從其他線程綁定UI元素,這會導(dǎo)致程序會有不確定的崩潰的可能。 - 結(jié)果綁定了兩個UI元素,這意味著對每次用戶的請求,都需要做兩次HTTP請求,每個UI元素一次,顯然這并不是我們想要的。
一個更加完整穩(wěn)定的版本應(yīng)該像這樣:
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)
在一個很大的系統(tǒng)里像這樣滿足每一個要求去寫出穩(wěn)定的程序是很有挑戰(zhàn)的,我們要介紹一個更簡單地方法,那就是Units。
下面這段代碼可以實(shí)現(xiàn)與上面的代碼相同的功能:
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轉(zhuǎn)化成一個Driver unit。
query.rx.text.asDriver()
在這里不需要做什么額外的處理,Driver有所有ControlProperty unit的屬性,而且增加了額外的屬性,整個observable序列被封裝成Driver,僅僅是這樣而已。
第二個改變是:
.asDriver(onErrorJustReturn: [])
任何一個observable序列只要有三個屬性,就可以轉(zhuǎn)化成Driverunit
- 不傳遞出錯誤
- 在主線程觀察
- 共享副作用 (
shareReplayLatestWhileConnected)
你如果滿足這些屬性呢?用Rx運(yùn)算符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或者其他接口調(diào)用drive方法,所以為了更加安全,你可以在綁定UI元素之前,創(chuàng)建一個暫時的定義let results: Driver<[Results]> = ...,我們讓讀者自己去判斷需不需要這樣子做。