RxSwift 中的 Units——一個富有哲學(xué)意味的概念(翻譯四)

單位(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ī)制),例如:

對于Drivermap運(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

這段代碼的問題在哪里?:

  • 如果fetchAutoCompleteItems observable序列發(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

driveDriver unit定義的方法,這意味著如果你在代碼中看見了drive,那就意味著那個observable序列不會傳遞出錯誤,在主線程被觀察,可以安全地綁定UI元素

理論上來講,可以對ObservableType或者其他接口調(diào)用drive方法,所以為了更加安全,你可以在綁定UI元素之前,創(chuàng)建一個暫時的定義let results: Driver<[Results]> = ...,我們讓讀者自己去判斷需不需要這樣子做。

原文鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容