MVVM 和 响应式编程入门

MVVM 和 响应式编程入门

架构的思考

简要概括一下 App 做的事?
架构是干什么的?

App 的本质是反馈回路

一个项目,最重要的两个角色:Model 和 View。

Model 决定是什么(数据),View 决定如何展示,其他的内容,基本都是处理两者的交互关系,以及提供这两者的服务。
如果要将 MV(X),其实基本讨论都逃不过两者间的交互:

反馈回路:

App 的任务

基于上图呢,我们的大部分的工作其实可以拆分到下面 5 项中的一项:

  1. 构建:谁负责构建 model 和 view,以及将两者连接起来?
  2. 更新 model:如何处理 view action?
  3. 改变 View:如何将 model 的数据应用到 view 上去?
  4. view state:如何处理导航和其他一些 model state(按钮状态、switch)
  5. 测试:为了达到一定程度的测试覆盖,要采取怎样的测试策略

对于上面5个问题的回答,构成了 App 设计模式的基础要件

view state 想想页面跳转的逻辑,按钮是否可点击的状态

回顾 MVC

MVC 其实在上面的反馈回路中间插入了 Controller,使得每条线都经过了 Controller。

MVC 模块职责

  1. 构建:谁负责构建 model 和 view,以及将两者连接起来?
  2. 更新 model:如何处理 view action?
  3. 改变 View:如何将 model 的数据应用到 view 上去?(单向数据流)

单向数据流:比如更改了用户姓名,其实应该是只负责更新 model 也就是 name 字段,然后通过 kvo,让响应的nameLabel 改变显示

  1. view state:如何处理导航和其他一些 model state
  2. 测试:为了达到一定程度的测试覆盖,要采取怎样的测试策略(集成测试)

MVC 总结

最简单的模式,适用于绝大部分情况。

两个地方不太尽人意:

  1. 观察者模式失效

举个例子:这段代码有什么潜在的问题?

func changeName(name : String) {
    person.name = name
    namelabel.text = name
}
  1. 肥大的 View Controller

责任重大,所以代码量非常容易就动辄几千行

对于这两个问题,其实也有很多的解决办法,第一个比如单向数据流,严格执行观察者模式;第二个办法非常非常多,比如 catogory,代理,代码拆分。(objc中国、唐巧)

MVC 是万能的吗?

为什么还要学习下其他的架构?

  • 借鉴思想弥补 MVC
  • 定义好框架严格执行
  • 锻炼设计、抽象的能力
  • 拓宽思维
    。。。

MVVM - C

Model - View - ViewModel + 协调器(Coordinator)

从 MVC 到 MVVM

用过 MVVM的话 觉得这张图有什么问题吗?

注意几点:

  1. 必须创建 View-model。
  2. 必须建立起 View-model 和 View 之间的绑定。
  3. Model 由 View-model 拥有,而不是由 controller 拥有。

学习一下代码,看是怎么跑起来的

先演示一下 Demo,并看一下 MVVM - C 个模块职责

协调器:

  1. 负责将 Model 的初始值,赋值给 rootViewController
  2. 所有和页面跳转的相关逻辑,(同时提供了新页面所需要的数据)

ViewModel

  1. 持有 model
  2. 拥有一系列可观察的信号量(序列)
  3. 提供数据的更新方法
  4. 私有辅助方法

ViewController & View

  1. 绑定 ViewModel的序列,和 View 的某个字段
  2. 将一些 ViewAction 调用的方法指向 ViewModel 中的数据更新方法
  3. 保存 View State
  4. 维护 View 的层级(demo中是 Storyboard)
  5. View 如何展示 (如何)

返回回路,已经基本被摘出去了

Model

还是那个 model

这样我们有个一个完整的管道

  1. 协调器协调了跳转以及为每个要跳转的 ViewController 的 ViewModel 设置初始的 model。
  2. ViewModel 使用 model提供直接可使用的可观察序列。(什么是观察序列?
  3. 为了能直接使用,需要干两间事:合并序列以及数据变形。(什么叫能直接使用?
  4. Controller 使用 bind 将准备好的值绑定到各个 View 上去。

来看反馈回路:

  1. TableView 发送 Action
tableView.rx.modelDeleted(Item.self)
			.subscribe(onNext: { [unowned self] in self.viewModel.deleteItem($0) }).disposed(by: disposeBag)
  1. Controller 调用 ViewModel 的方法,来删除数据
func deleteItem(_ item: Item) {
	folder.value.remove(item)
}
  1. 调用持久化层的 save 更改数据,产生一个通知
NotificationCenter.default.post(name: Store.changedNotification, object: notifying, userInfo: userInfo)
  1. 通知早已经作为一个序列,已经被其他序列合并,造成多个序列的更新事件。
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 的显示或者状态。

  1. 通过之前的 bind 更新 view 的显示
viewModel.folderContents.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)

怎么就绑定了?

先来看下函数响应式编程

函数响应式编程

先来看个例子

一个拥有用户名和密码输入框的登录界面:

产品经理说了需求:4句话:

  1. 用户名不足 5 个字符的时候,给出红色提示语1;
  2. 用户名不足 5 个字符的时候,无法输入密码,>=5时,可以输入
  3. 密码不足 5 个时候,也显示红色提示语2;
  4. 用户名和密码有一个不符合要求时,底部绿色按钮不可点击,只有当用户名和密码同时有效时按钮才可以点击。

一般的思路:

监听 Username 输入框,根据字符个数要考虑下面3件事:

  1. 提示语1是否显示
  2. Password 是否可输入
  3. 结合 Password 的状况,判断按钮是否可以点击

监听 Password 输入框,根据字符个数考虑下面2件事:

  1. 提示语2是否显示
  2. 结合 UserName 的状况,判断按钮是否可以点击

所以开发过程:

有什么问题?

  1. 需要翻译这个过程
  2. 很多变化需要结合到一起考虑,如果变化因素更多了,很容易出 bug。

其实这个翻译过程,我们自己把一些有联系的因素给放到一起处理了,比如按钮是都可点击,需要同时监听两个文本框的状态。

我们能否只罗列条件,然后把这些条件扔给一个条件处理的机制,这个机制就能帮我们正确的处理这些关系?

函数响应式编程来了:

我们做两件事情:

  1. 将条件作为对象(序列)
  2. 将条件和结果进行绑定

开发过程变成了:

来看看代码:

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 度时,打开空调降温。

函数响应式编程里最重要的就是构造序列。在函数响应式编程中,一切都可以看作是序列。

  1. 一次点击事件
  2. 一个属性的变化
  3. 一个网络请求回调
  4. 。。。。

其实对于值的变化的队列,好理解,那么一次操作,或者一次网络请求任务也看做是队列,这个怎么实现的?

大多数序列可以产生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 总结

  1. 很大程度上解决了 MVC 的痛点
  2. 响应式编程双刃剑,学习成本陡峭,但是用起来会非常爽,整体代码甚至会减少,bug也会减少,调试变得困难。

较少响应式编程的 MVVM

  1. tableview的 代理
  2. 使用 Notification 和 KVO 代替响应式编程

经验和教训

即使我们不使用 MVVM 他的一些思想我们还是可以借鉴的。

  1. 引入中间层
  2. 协调器,解耦多个 Controller
  3. 数据变形是单独可以提出来的

引用:

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

posted @ 2019-04-09 16:49  张驰小方块  阅读(1238)  评论(0编辑  收藏  举报