为何使用Rx
Rx支持以声明方式构建应用程序。
绑定
// 原文这里应该有纰漏,应为firstName.rx.text.orEmpty与lastName.rx.text.orEmpty否则会造成可选字符串未解包
Observable.combineLatest(firstName.rx.text, lastName.rx.text) { $0 + " " + $1 }
.map { "Greetings, \($0)" }
.bind(to: greetingLabel.rx.text)
也可以绑定UITableView和UICollectionView。
viewModel
.rows
.bind(to: resultsTableView.rx.items(cellIdentifier: "WikipediaSearchCell", cellType: WikipediaSearchCell.self)) { (_, viewModel, cell) in
cell.title = viewModel.title
cell.url = viewModel.url
}
.disposed(by: disposeBag)
官方建议总是使用.disposed(by: disposeBag)即使这对简单绑定不是必需的。
重试
如果API不会失败会很棒,但遗憾的是它们会失败。假设有一个API方法:
func doSomethingIncredible(forWho: String) throws -> IncredibleThing
如果按原样使用此函数,则在失败时很难进行重试。更不用说建模嵌套调用的复杂性。当然有可能,但代码可能包含许多不关心的瞬态,并且它不可重复使用。
理想情况下,需要捕获重试的本质,并能够将其应用于任何操作。
这是使用Rx进行简单重试的方法
doSomethingIncredible("me")
.retry(3)
还可以轻松创建自定义重试操作。
代理
代替冗长和难以表示的代码
public func scrollViewDidScroll(scrollView: UIScrollView) { [weak self]
self?.leftPositionConstraint.constant = scrollView.contentOffset.x
}
Rx实现
self.resultsTableView
.rx.contentOffset
.map { $0.x }
.bind(to: self.leftPositionConstraint.rx.constant)
KVO
代替:
对象销毁了然而键值观察还注册着,观测信息被泄露,甚至可能被错误地附加到其他对象上。
和
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
使用rx.observe和rx.observeWeakly
这是他们的使用方式:
view.rx.observe(CGRect.self, "frame")
.subscribe(onNext: { frame in
print("Got new frame \(frame)")
})
.disposed(by: disposeBag)
或者
someSuspiciousViewController
.rx.observeWeakly(Bool.self, "behavingOk")
.subscribe(onNext: { behavingOk in
print("Cats can purr? \(behavingOk)")
})
.disposed(by: disposeBag)
通知
代替使用:
@available(iOS 4.0, *)
public func addObserverForName(name: String?, object obj: AnyObject?, queue: NSOperationQueue?, usingBlock block: (NSNotification) -> Void) -> NSObjectProtocol
仅仅编写
NotificationCenter.default
.rx.notification(NSNotification.Name.UITextViewTextDidBeginEditing, object: myTextView)
.map { /*使用数据做一些事情*/ }
....
瞬态
编写异步程序时,瞬态存在很多问题。典型示例是自动完成的搜索框。
如果没有Rx要编写自动完成的搜索框代码,那么可能需要解决的第一个问题是当输入abc中的c时,有一个待处理的请求ab,需要取消待处理的请求。好吧,这应该不太难解决,只需创建一个额外的变量来保持对待处理请求的引用。
下一个问题是如果请求失败,则需要执行那种混乱的重试逻辑。但是好吧,增加字段捕获需要处理的重试次数。
如果程序需要在向服务器发出请求之前等待一段时间。毕竟,因为不想有人在长时间输入时太频繁的请求服务器。可能需要添加一个计时字段?
还有一个问题是,在执行搜索时屏幕上需要显示什么,以及即使重试也会失败的情况下需要显示的内容。
写下所有这些并正确地测试它将是乏味的。这是用Rx编写的相同逻辑。
searchTextField.rx.text
.throttle(.milliseconds(300), scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest { query in
API.getSearchResults(query)
.retry(3)
.startWith([]) // 在新的搜索中清除结果
.catchErrorJustReturn([])
}
.subscribe(onNext: { results in
// 绑定到UI
})
.disposed(by: disposeBag)
组合处理
假设希望在表视图中显示模糊图像。首先,应从URL中提取图像,然后解码然后模糊。
单元格退出表视图的可视区域可以取消整个过程是很好的,因为处理模糊的开销非常大。
在单元格进入可见区域后不立即开始获取图像也是很好的,因为如果用户滑动非常快,可能会有很多请求被触发和取消。
如果我们可以限制并发图像操作的数量也很好,同样因为模糊图像是一项开销非常大的操作。
这就是如何使用Rx来做到这一点:
// 这是一个概念性的解决方案
let imageSubscription = imageURLs
.throttle(.milliseconds(200), scheduler: MainScheduler.instance)
.flatMapLatest { imageURL in
API.fetchImage(imageURL)
}
.observeOn(operationScheduler)
.map { imageData in
return decodeAndBlurImage(imageData)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { blurredImage in
imageView.image = blurredImage
})
.disposed(by: reuseDisposeBag)
此代码将完成所有这些操作,并且在imageSubscription完成处理时,它将取消所有相关的异步操作,并确保没有恶意图像绑定到UI。
聚合网络请求
如果需要触发两个请求并在两个请求完成时汇总结果,该怎么办?
zip操作符
let userRequest: Observable<User> = API.getUser("me")
let friendsRequest: Observable<[Friend]> = API.getFriends("me")
Observable.zip(userRequest, friendsRequest) { user, friends in
return (user, friends)
}
.subscribe(onNext: { user, friends in
// 绑定到界面
})
.disposed(by: disposeBag)
那么,如果这些API在后台线程上返回结果,并且绑定UI必须在主线程上进行呢?可以使用observeOn。
let userRequest: Observable<User> = API.getUser("me")
let friendsRequest: Observable<[Friend]> = API.getFriends("me")
Observable.zip(userRequest, friendsRequest) { user, friends in
return (user, friends)
}
.observeOn(MainScheduler.instance)
.subscribe(onNext: { user, friends in
// bind them to the user interface
})
.disposed(by: disposeBag)
还有更多使用Rx发挥作用的实际用例。
状态
允许变化的语言可以轻松访问全局状态并对其进行更改。共享全局状态的不受控制的更改很容易导致组合爆炸。
但另一方面,当以智能方式使用时,命令式语言可以编写更高效更接近硬件的代码。
处理组合爆炸的常用方法是保持状态尽可能简单,并使用单向数据流来模拟派生数据。
这是Rx真正发挥作用的地方。
Rx是函数式和命令式的最佳结合点。它能够使用不可变定义和纯函数以可靠的可组合方式处理可变状态。
那么,有哪些实际例子呢?
易于集成
如果需要创建自己的可观察对象怎么办?这很容易。这段代码来自RxCocoa,这就是需要用URLSession包装HTTP请求的全部内容
extension Reactive where Base: URLSession {
public func response(request: URLRequest) -> Observable<(Data, HTTPURLResponse)> {
return Observable.create { observer in
let task = self.base.dataTask(with: request) { (data, response, error) in
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
observer.on(.next(data, httpResponse))
observer.on(.completed)
}
task.resume()
return Disposables.create(with: task.cancel)
}
}
}
优点
简而言之,使用Rx将使代码:
- 可组合 <- 因为Rx是组合的代名词
- 可重用 <- 因为它是可组合的
- 声明性 <- 因为定义是不可变的,只有数据发生变化
- 可理解和简洁 <- 提高抽象级别并消除瞬态
- 稳定 <- 因为Rx代码经过了彻底的单元测试
- 状态较少 <- 因为将应用程序建模为单向数据流
- 没有泄漏 <- 因为资源管理很容易
这并不是全部
使用Rx对尽可能多的应用程序进行建模通常是个好主意。
但是,如果不了解所有操作符以及还不存在某些操作符对特定情况进行建模,该怎么办?
好吧,所有Rx操作符都基于数学,应该是直观的。
好消息是大约10-15个操作符覆盖了大多数典型的用例。这名单已经包括了一些熟悉的像的map,filter,zip,observeOn,...
对于每个操作符,都有一个纹理图,有助于解释它是如何工作的。
但是,如果需要一些不在该列表中的操作符,该怎么办?那么,你可以建立自己的操作符。
如果由于某种原因创建那种操作符真的很难,或者你需要使用一些遗留的有状态代码,该怎么办?好吧,已经弄得一团糟,但可以轻松跳出Rx环境,处理数据,然后返回到它。
网友评论