MVVM 和 响应式编程入门
MVVM 和 响应式编程入门
架构的思考
简要概括一下 App 做的事?
架构是干什么的?
App 的本质是反馈回路
一个项目,最重要的两个角色:Model 和 View。
Model 决定是什么(数据),View 决定如何展示,其他的内容,基本都是处理两者的交互关系,以及提供这两者的服务。
如果要将 MV(X),其实基本讨论都逃不过两者间的交互:
反馈回路:
App 的任务
基于上图呢,我们的大部分的工作其实可以拆分到下面 5 项中的一项:
- 构建:谁负责构建 model 和 view,以及将两者连接起来?
- 更新 model:如何处理 view action?
- 改变 View:如何将 model 的数据应用到 view 上去?
- view state:如何处理导航和其他一些 model state(按钮状态、switch)
- 测试:为了达到一定程度的测试覆盖,要采取怎样的测试策略
对于上面5个问题的回答,构成了 App 设计模式的基础要件
view state 想想页面跳转的逻辑,按钮是否可点击的状态
回顾 MVC
MVC 其实在上面的反馈回路中间插入了 Controller,使得每条线都经过了 Controller。
MVC 模块职责
- 构建:谁负责构建 model 和 view,以及将两者连接起来?
- 更新 model:如何处理 view action?
- 改变 View:如何将 model 的数据应用到 view 上去?(单向数据流)
单向数据流:比如更改了用户姓名,其实应该是只负责更新 model 也就是 name 字段,然后通过 kvo,让响应的nameLabel 改变显示
- view state:如何处理导航和其他一些 model state
- 测试:为了达到一定程度的测试覆盖,要采取怎样的测试策略(集成测试)
MVC 总结
最简单的模式,适用于绝大部分情况。
两个地方不太尽人意:
- 观察者模式失效
举个例子:这段代码有什么潜在的问题?
func changeName(name : String) {
person.name = name
namelabel.text = name
}
- 肥大的 View Controller
责任重大,所以代码量非常容易就动辄几千行
对于这两个问题,其实也有很多的解决办法,第一个比如单向数据流,严格执行观察者模式;第二个办法非常非常多,比如 catogory,代理,代码拆分。(objc中国、唐巧)
MVC 是万能的吗?
为什么还要学习下其他的架构?
- 借鉴思想弥补 MVC
- 定义好框架严格执行
- 锻炼设计、抽象的能力
- 拓宽思维
。。。
MVVM - C
Model - View - ViewModel + 协调器(Coordinator)
从 MVC 到 MVVM
用过 MVVM的话 觉得这张图有什么问题吗?
注意几点:
- 必须创建 View-model。
- 必须建立起 View-model 和 View 之间的绑定。
- Model 由 View-model 拥有,而不是由 controller 拥有。
学习一下代码,看是怎么跑起来的
先演示一下 Demo,并看一下 MVVM - C 个模块职责
协调器:
- 负责将 Model 的初始值,赋值给 rootViewController
- 所有和页面跳转的相关逻辑,(同时提供了新页面所需要的数据)
ViewModel
- 持有 model
- 拥有一系列可观察的信号量(序列)
- 提供数据的更新方法
- 私有辅助方法
ViewController & View
- 绑定 ViewModel的序列,和 View 的某个字段
- 将一些 ViewAction 调用的方法指向 ViewModel 中的数据更新方法
- 保存 View State
- 维护 View 的层级(demo中是 Storyboard)
- View 如何展示 (如何)
返回回路,已经基本被摘出去了
Model
还是那个 model
这样我们有个一个完整的管道
- 协调器协调了跳转以及为每个要跳转的 ViewController 的 ViewModel 设置初始的 model。
- ViewModel 使用 model提供直接可使用的可观察序列。(什么是观察序列?)
- 为了能直接使用,需要干两间事:合并序列以及数据变形。(什么叫能直接使用?)
- Controller 使用 bind 将准备好的值绑定到各个 View 上去。
来看反馈回路:
- TableView 发送 Action
tableView.rx.modelDeleted(Item.self)
.subscribe(onNext: { [unowned self] in self.viewModel.deleteItem($0) }).disposed(by: disposeBag)
- Controller 调用 ViewModel 的方法,来删除数据
func deleteItem(_ item: Item) {
folder.value.remove(item)
}
- 调用持久化层的 save 更改数据,产生一个通知
NotificationCenter.default.post(name: Store.changedNotification, object: notifying, userInfo: userInfo)
- 通知早已经作为一个序列,已经被其他序列合并,造成多个序列的更新事件。
var folderContents: Observable<[AnimatableSectionModel<Int, Item>]> {
return folderUntilDeleted.map { folder in
guard let f = folder else {
return [AnimatableSectionModel(model: 0, items: [])]
}
return [AnimatableSectionModel(model: 0, items: f.contents)]
}
}
注意,和通知序列相关的好多序列都会收到更新,从而更新各种 View 的显示或者状态。
- 通过之前的 bind 更新 view 的显示
viewModel.folderContents.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
怎么就绑定了?
先来看下函数响应式编程
函数响应式编程
先来看个例子
一个拥有用户名和密码输入框的登录界面:
产品经理说了需求:4句话:
- 用户名不足 5 个字符的时候,给出红色提示语1;
- 用户名不足 5 个字符的时候,无法输入密码,>=5时,可以输入
- 密码不足 5 个时候,也显示红色提示语2;
- 用户名和密码有一个不符合要求时,底部绿色按钮不可点击,只有当用户名和密码同时有效时按钮才可以点击。
一般的思路:
监听 Username 输入框,根据字符个数要考虑下面3件事:
- 提示语1是否显示
- Password 是否可输入
- 结合 Password 的状况,判断按钮是否可以点击
监听 Password 输入框,根据字符个数考虑下面2件事:
- 提示语2是否显示
- 结合 UserName 的状况,判断按钮是否可以点击
所以开发过程:
有什么问题?
- 需要翻译这个过程
- 很多变化需要结合到一起考虑,如果变化因素更多了,很容易出 bug。
其实这个翻译过程,我们自己把一些有联系的因素给放到一起处理了,比如按钮是都可点击,需要同时监听两个文本框的状态。
我们能否只罗列条件,然后把这些条件扔给一个条件处理的机制,这个机制就能帮我们正确的处理这些关系?
函数响应式编程来了:
我们做两件事情:
- 将条件作为对象(序列)
- 将条件和结果进行绑定
开发过程变成了:
来看看代码:
let usernameValid = usernameOutlet.rx.text.orEmpty
.map { $0.characters.count >= minimalUsernameLength }
.share(replay: 1) // without this map would be executed once for each binding, rx is stateless by default
let passwordValid = passwordOutlet.rx.text.orEmpty
.map { $0.characters.count >= minimalPasswordLength }
.share(replay: 1)
let everythingValid = Observable.combineLatest(usernameValid, passwordValid) { $0 && $1 }
.share(replay: 1)
usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)
usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)
passwordValid
.bind(to: passwordValidOutlet.rx.isHidden)
.disposed(by: disposeBag)
everythingValid
.bind(to: doSomethingOutlet.rx.isEnabled)
.disposed(by: disposeBag)
doSomethingOutlet.rx.tap
.subscribe(onNext: { [weak self] _ in self?.showAlert() })
.disposed(by: disposeBag)
无需翻译,只需要罗列条件,接下来就是见证奇迹的时刻
直观的来看代码清晰很多,然后不怎么用动脑子,我们下面来看看什么是函数响应式编程再来说明他有哪些优缺点。
函数式编程
函数式编程是种编程范式
,它需要我们将函数作为参数传递,或者作为返回值返还。我们可以通过组合不同的函数来得到想要的结果。
通过函数这个“管道”,数据从一头经过“管道”到另一头,就得到了想要的数据。
编程范式?(命令式、声明式、函数式)https://www.cnblogs.com/sirkevin/p/8283110.html
函数响应式编程
函数式编程 + 响应
通过函数构建数据序列,最后通过适当的方式来响应这个序列,就是函数响应式编程。
在 Swift 中,我们是用 RxSwift 来实现函数响应式编程!
RxSwift 核心
核心角色有以下5个
- Observable - 可被监听的序列 - 产生事件
- Observer - 观察者 - 响应事件
- Operator - 操作符 - 创建变化组合事件
- Disposable - 可被清楚的资源 - 管理绑定(订阅)的生命周期
- Schedulers - 调度器 - 线程队列调配
如下图所示:
Observable 可被监听的序列(下面都简称序列)
一个序列,随着时间的流逝,这个队列将陆续产生一些可以被观察的值。
你可以将温度看作是一个序列,然后监测这个序列产生的值,最后对这个值做出响应。例如:当室温高于 33 度时,打开空调降温。
函数响应式编程里最重要的就是构造序列。在函数响应式编程中,一切都可以看作是序列。
- 一次点击事件
- 一个属性的变化
- 一个网络请求回调
- 。。。。
其实对于值的变化的队列,好理解,那么一次操作,或者一次网络请求任务也看做是队列,这个怎么实现的?
大多数序列可以产生3种事件:
public enum Event<Element> {
case next(Element)
case error(Swift.Error)
case completed
}
- next - 序列产生了一个新的元素
- error - 创建序列时产生了一个错误,导致序列终止
- completed - 序列的所有元素都已经成功产生,整个序列已经完成
所以当你想任何东西封装成一个序列,只要 create 一个序列,然后在原有逻辑基础上在适当的时机调用这些事件即可,并且 RxSwift 已经帮我们创建了大量的序列:
button 的点击
textField 的当前文本
switch 的开关状态
Notification 队列
如果自己创建,就需要调用上面说的事件了,比如我们手动创建一个序列:
let numbers: Observable<Int> = Observable.create { observer -> Disposable in
observer.onNext(0)
observer.onNext(1)
observer.onNext(2)
observer.onNext(3)
observer.onNext(4)
observer.onNext(5)
observer.onNext(6)
observer.onNext(7)
observer.onNext(8)
observer.onNext(9)
observer.onCompleted() // 结束
return Disposables.create()
}
除了普通的队列,框架还提供了好多其他的队列提供了不同的特性。
- Single:它要么只能发出一个元素,要么产生一个 error 事件。
- Completable :它要么只能产生一个 completed 事件,要么产生一个 error 事件。
- Maybe:它介于 Single 和 Completable 之间,它要么只能发出一个元素,要么产生一个 completed 事件,要么产生一个 error 事件。
- 还有 Driver、ControlEvent
一个序列,其实就是一个被观察者
观察者
观察者是:观察序列的,响应序列事件的角色
创建一个观察者:
tap.subscribe(onNext: { [weak self] in
self?.showAlert()
}, onError: { error in
print("发生错误: \(error.localizedDescription)")
}, onCompleted: {
print("任务完成")
})
创建观察者最直接的方法就是在 Observable 的 subscribe 方法后面描述,事件发生时,需要如何做出响应。而观察者就是由后面的 onNext,onError,onCompleted的这些闭包构建出来的。
同样框架为我们提供了很多观察者,几乎每个类的所有属性(包括自定义类),class.rx.xx 都可以作为观察者。
viewModel.navigationTitle.bind(to: rx.title).disposed(by: disposeBag)
viewModel.noRecording.bind(to: activeItemElements.rx.isHidden).disposed(by: disposeBag)
viewModel.hasRecording.bind(to: noRecordingLabel.rx.isHidden).disposed(by: disposeBag)
viewModel.timeLabelText.bind(to: progressLabel.rx.text).disposed(by: disposeBag)
viewModel.durationLabelText.bind(to: durationLabel.rx.text).disposed(by: disposeBag)
viewModel.sliderDuration.bind(to: progressSlider.rx.maximumValue).disposed(by: disposeBag)
viewModel.sliderProgress.bind(to: progressSlider.rx.value).disposed(by: disposeBag)
viewModel.playButtonTitle.bind(to: playButton.rx.title(for: .normal)).disposed(by: disposeBag)
viewModel.nameText.bind(to: nameTextField.rx.text).disposed(by: disposeBag)
Binder
观察者有两种:我们这次只讲下 Binder
- AnyObserver
- Binder
Binder 主要有以下两个特征:
- 不会处理错误事件
- 确保绑定都是在给定 Scheduler 上执行(默认 MainScheduler)
所以很多UI 观察者都使用 Binder 去实现,只处理 Next ,并且在 主线程响应。
usernameValidOutlet.rx.isHidden 的由来
由于页面是否隐藏是一个常用的观察者,所以应该让所有的 UIView 都提供这种观察者:
extension Reactive where Base: UIView {
public var isHidden: Binder<Bool> {
return Binder(self.base) { view, hidden in
view.isHidden = hidden
}
}
}
usernameValid
.bind(to: usernameValidOutlet.rx.isHidden)
.disposed(by: disposeBag)
这样你不必为每个 UI 控件单独创建该观察者。这就是 usernameValidOutlet.rx.isHidden 的由来,许多 UI 观察者 都是这样创建的。
操作符
有了序列(被观察者 Observable)和观察者 (Observer),还差一点什么?
我有了一个时间戳的序列,怎么和观察者(birthdayLabel.rx.text)绑定?
我有了 Username 和 Password 是否有效的序列,怎么和 Button 是否可以点击的观察者(doSomethingOutlet.rx.isEnabled)绑定?
序列需要变形、合并、相互影响!
操作符可以帮助大家创建新的序列,或者变化组合原有的序列,从而生成一个新的序列。
https://beeth0ven.github.io/RxSwift-Chinese-Documentation/content/decision_tree.html
这里只介绍一种最简单常用的操作符,map,知道操作符的含义即可。
let usernameValid = usernameOutlet.rx.text.orEmpty
.map { $0.characters.count >= minimalUsernameLength }
.share(replay: 1)
Disposable
既然以后绑定和订阅,肯定有取消绑定和订阅,怎么取消呢,就用到了 Disposable。
最常用的是清除包(DisposeBag) 或者 takeUntil 操作符 来管理订阅的生命周期。
var disposeBag = DisposeBag() // 来自父类 ViewController
override func viewDidLoad() {
super.viewDidLoad()
...
usernameValid
.bind(to: passwordOutlet.rx.isEnabled)
.disposed(by: disposeBag)
}
这个例子中 disposeBag 和 ViewController 具有相同的生命周期。当退出页面时, ViewController 就被释放,disposeBag 也跟着被释放了,那么这里的绑定(订阅)也就被取消了。这正是我们所需要的。
调度器
Schedulers 是 Rx 实现多线程的核心模块,它主要用于控制任务在哪个线程或队列运行。
let rxData: Observable<Data> = ...
rxData
// 序列的构建函数在后台运行
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
// 主线程监听和处理结果
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] data in
self?.data = data
})
.disposed(by: disposeBag)
函数响应式编程总结
个人感悟:
序列、操作符、观察者分别被封装成,交互过程中的3个对象,将这3者进行了解耦。可以灵活的组合,对序列进行变形、合并生成新的可直接使用的序列。提供一种更为高效的编程方法,从数据变形的角度看是一种降维,所以这方面的 Bug 会少一些。同时,学习曲线过于陡峭,用不好很可能写出很反人类的代码,让别人根本没法读,另外使用绑定机制,出现一些 Bug 确实不易调试。
但是最可贵的是这种思想,我们还可以把函数看做是对象,可以作为参数传递,可以作为返回值,就像一些设计模式,将算法封装成对象,有了策略模式;将命令封装成对象,有了命令模式;将状态封装成对象,有了状态模式,都解决了一些领域里的问题。学习这种思想,让我们以后在编码上能够已更加宽广的视角去看待问题。
回头来看 MVVM 中的绑定
init(initialFolder: Folder = Store.shared.rootFolder) {
folder = Variable(initialFolder)
folderUntilDeleted = folder.asObservable()
// Every time the folder changes
.flatMapLatest { currentFolder in
// Start by emitting the initial value
Observable.just(currentFolder)
// Re-emit the folder every time a non-delete change occurs
.concat(currentFolder.changeObservable.map { _ in currentFolder })
// Stop when a delete occurs
.takeUntil(currentFolder.deletedObservable)
// After a delete, set the current folder back to `nil`
.concat(Observable.just(nil))
}.share(replay: 1)
}
folder.asObservable()
对一个属性生成一个序列的方式
flatMapLatest
“在数据源每次发出一个值的时候,它使用该值构建,开始,或者选择一个新的可观察量。不过这个变形可以让>我们基于第一个可观察量发出的状态,来订阅第二个可观察量。”
—— 摘录来自: Chris Eidhof. “App 架构。” iBooks.
just
concat
让两个或者多个 Observables 按顺序串联起来
concat 操作符将多个 Observables
按顺序串联起来,当前一个 Observable
元素发送完毕后,后一个 ``Observable` 才可以开始发出元素。
concat 将等待前一个 Observable 产生完成事件后,才对后一个 Observable 进行订阅。如果后一个是“热” Observable ,在它前一个 Observable 产生完成事件前,所产生的元素将不会被发送出来。
currentFolder.changeObservable
var changeObservable: Observable<()> {
return NotificationCenter.default.rx.notification(Store.changedNotification).filter { [weak self] (note) -> Bool in
guard let s = self else { return false }
if let item = note.object as? Item, item == s, !(note.userInfo?[Item.changeReasonKey] as? String == Item.removed) {
return true
} else if let userInfo = note.userInfo, userInfo[Item.parentFolderKey] as? Folder == s {
return true
}
return false
}.map { _ in () }
}
每次收到通知,只有经过 filter 函数检验为 true 的元素,才会被放到序列中作为新事件。
takeUntil
currentFolder.deletedObservable
同 currentFolder.changeObservable,这不过这个是和删除有关的通知,才会放到序列。
concat(Observable.just(nil))
nil 将会在 takeUtil 执行后发出,想想为啥?
share(replay: 1)
多次绑定只执行一次操作序列
folderUntilDeleted
我们将 folder 这个可观察量,与其他由 model 驱动的,可能影响我们 view 的逻辑的可观察量,进行了合并。得到的结果是一个新的可观察量 folderUntilDeleted,它会在底层文件夹对象发生变化时正确更新,并且在底层文件夹对象被从 store 中删除时将自己设置为 nil。
MVVM-C 总结
- 很大程度上解决了 MVC 的痛点
- 响应式编程双刃剑,学习成本陡峭,但是用起来会非常爽,整体代码甚至会减少,bug也会减少,调试变得困难。
较少响应式编程的 MVVM
- tableview的 代理
- 使用 Notification 和 KVO 代替响应式编程
经验和教训
即使我们不使用 MVVM 他的一些思想我们还是可以借鉴的。
- 引入中间层
- 协调器,解耦多个 Controller
- 数据变形是单独可以提出来的
引用:
RxSwift 中文文档:https://beeth0ven.github.io/RxSwift-Chinese-Documentation/
《App 架构》
Interactive diagrams of Rx Observables : http://rxmarbles.com
菜鸟教程 swift 教程: https://www.runoob.com/swift/swift-tutorial.html