RxSwift 中的 Units——一个富有哲学意味的概念(翻译四)

单位(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序列的运算来定义(事实上这也是运算符工作的内部机制),例如:

对于Drivermap运算就是对于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序列只要有三个属性,就可以转化成Driverunit

  • 不传递出错误
  • 在主线程观察
  • 共享副作用 (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

driveDriver unit定义的方法,这意味着如果你在代码中看见了drive,那就意味着那个observable序列不会传递出错误,在主线程被观察,可以安全地绑定UI元素

理论上来讲,可以对ObservableType或者其他接口调用drive方法,所以为了更加安全,你可以在绑定UI元素之前,创建一个暂时的定义let results: Driver<[Results]> = ...,我们让读者自己去判断需不需要这样子做。

原文链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容