前段时间在RxSwift上作了一些实践,Rx确实是一个强大的工具,但同时也是一把双刃剑,若是滥用的话反而会带来反作用,本文就引入Rx模式以后如何更好的管理应用的状态和逻辑作了一些粗浅的总结。前端
本文篇幅较长,主要围绕着状态管理这一话题进行介绍,前两个部分介绍了前端领域中React和Vue所采用的状态管理模式及其在Swift中的实现,最后介绍了另外一种简化的状态管理方案。不会涉及复杂的Rx特性,阅读前对Rx有一些基本的了解便可。react
一个复杂的页面一般须要维护大量的变量来表示其运行期间的各类状态,在MVVM中页面大部分的状态和逻辑都经过ViewModel来维护,在常见的写法中ViewModel和视图之间一般用Delegate
来通信,好比说在数据改变的时候通知视图层更新UI等等:git
在这种模式中,ViewModel的状态更新以后须要咱们调用Delegate手动通知视图层。而在Rx中这一层关系被淡化了,因为Rx是响应式的,设定好绑定关系后ViewModel只须要改变数据的值,Rx会自动的通知每个观察者:github
Rx为咱们隐藏了通知视图的过程,首先这样的好处是明显的:ViewModel能够更加专一于数据自己,不用再去管UI层的逻辑;可是滥用这个特性也会带来麻烦,大量的可观察变量和绑定操做会让逻辑变得含糊不清,修改一个变量的时候可能会致使一系列难以预料的连锁反应,这样代码反而会变得更加难以维护。编程
想要更好的过渡到响应式编程,一个统一的状态管理方案是不可或缺的。在这一块前端领域有很多成熟的实践方案,Swift中也有一些开源库对其进行了实现,其中的思想咱们能够先来参考一下。redux
下面的介绍中所涉及的示例代码在:github.com/L-Zephyr/My…。swift
Redux
是Facebook所提出的基于Flux改良的一种状态管理模式,在Swift中有一个名为ReSwift的开源项目实现了这个模式。后端
要理解Redux首先要明白Redux是为了解决什么问题而生的,Redux为应用提供统一的状态管理,并实现了单向的数据流。所谓的单向绑定
和双向绑定
所描述的都是视图(View)和数据(Model)之间的关系:前端框架
比方说有一个展现消息的页面,首先须要从网络加载最新的消息,在MVC中咱们能够这样写:网络
class NormalMessageViewController: UIViewController {
var msgList: [MsgItem] = [] // 数据源
// 网络请求
func request() {
// 1. 开始请求前播放loading动画
self.startLoading()
MessageProvider.request(.news) { (result) in
switch result {
case .success(let response):
if let list = try? response.map([MsgItem].self) {
// 2. 请求结束后更新model
self.msgList = list
}
case .failure(_):
break
}
// 3. model更新后同步更新UI
self.stopLoading()
self.tableView.reloadData()
}
}
// ...
}
复制代码
还能够将不须要的消息从列表中删除:
extension NormalMessageViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// 1. 更新model
self.msgList.remove(at: indexPath.row)
// 2. 刷新UI
self.tableView.reloadData()
}
}
// ...
}
复制代码
在request
方法中咱们经过网络请求修改了数据msgList
,一旦msgList
发生改变必须刷新UI,显然视图的状态跟数据是同步的;在tableView上删除消息时,视图层直接对数据进行操做而后刷新UI。视图层即会响应数据改变的事件,又会直接访问和修改数据,这就是一个双向绑定的关系:
虽然在这个例子中看起来很是简单,可是当页面比较复杂的时候UI操做和数据操做混杂在一块儿会让逻辑变得混乱。看到这里单向绑定
的含义就很明显了,它去掉了View -> Model
的这一层关系,视图层不能直接对数据进行修改,它只能经过某种机制向数据层传递事件,并在数据改变的时候刷新UI。
为了构造单向数据流,Redux引入了一系列概念,这是Redux中所描述的数据流:
其中的State
就是应用的状态,也就是咱们的Model部分,先无论这里的Action
、Reducer
等概念,从图中能够看到State和View是有着直接的绑定关系的,而View的事件则会经过Action
、Store
等一系列操做间接的改变State
,下面来详细的介绍一下Redux的数据流的实现以及所涉及到的概念:
View 顾名思义,View就是视图,用户在视图上的操做事件不会直接修改模型,而是会被映射成一个个Action
。
Action Action
表示一个对数据操做的请求,Action会被发送到Store
中,这是对模型数据进行修改的惟一办法。
在ReSwift中有一个名为Action的协议(仅做标记用的空协议),对于Model中数据的每一个操做,好比说设置一个值,都须要有一个对应的Action:
/// 设置数据的Action
struct ActionSetMessage: Action {
var news: [MsgItem] = []
}
/// 移除某项数据的Action
struct ActionRemoveMessage: Action {
var index: Int
}
复制代码
用struct
类型来表示一个Action,Action所携带的数据保存在其成员变量中。
Store和State 就像上面所提到的,State
表示了应用中的Model数据,而Store
则是存放State的地方;在Redux中Store是一个全局的容器,全部组件的状态都被保存在里面;Store接受一个Action,而后修改数据并通知视图层更新UI。
以下所示,每个页面和组件都有各自的状态以及用来储存状态的Store:
// State
struct ReduxMessageState: StateType {
var newsList: [MsgItem] = []
}
// Store,直接使用ReSwift的Store类型来初始化便可,初始化时要指定reducer和状态的初始值
let newsStore = Store<ReduxMessageState>(reducer: reduxMessageReducer, state: nil)
复制代码
Store
经过一个dispatch
方法来接收Action
,视图调用这个方法来向Store传递Action:
messageStore.dispatch(ActionRemoveMessage(index: 0))
复制代码
Reducer Reducer
是一个比较特殊的函数,这里实际上是借鉴了函数式的一些思想,首先Redux强调了数据的不可变性(Immutable),简单来讲就是一个数据模型在建立以后就不可被修改,那当咱们要修改Model某个属性时要怎么办呢?答案就是建立一个新的Model,Reducer
的做用就体如今这里:
Reducer
是一个函数,它的签名以下:
(_ action: Action, _ state: StateType?) -> StateType
复制代码
接受一个表示动做的action和一个表示当前状态的state,而后计算并返回一个新的State,随后这个新的State会被更新到Store中:
// Store.swift中的实现
open func _defaultDispatch(action: Action) {
guard !isDispatching else {
raiseFatalError("...")
}
isDispatching = true
let newState = reducer(action, state) // 1. 经过reducer计算出新的state
isDispatching = false
state = newState // 2. 直接将新的state赋值到当前的state上
}
复制代码
应用中全部数据模型的更新操做最终都经过Reducer
来完成,为了保证这一套流程能够正常的完成,Reducer
必须是一个纯函数:它的输出只取决于输入的参数,不依赖任何外部变量,一样也不能包含任何异步的操做。
在这个例子中的Reducer
是这样写的:
func reduxMessageReducer(action: Action, state: ReduxMessageState?) -> ReduxMessageState {
var state = state ?? ReduxMessageState()
// 根据不一样的Action对数据进行相应的修改
switch action {
case let setMessage as ActionSetMessage: // 设置列表数据
state.newsList = setMessage.news
case let remove as ActionRemoveMessage: // 移除某一项
state.newsList.remove(at: remove.index)
default:
break
}
// 最后直接返回修改后的整个State结构体
return state
}
复制代码
最后在视图中实现StoreSubscriber
协议接收State改变的通知并更新UI便可。详细的代码请看Demo中的Redux
文件夹。
Redux将View -> Model
这一层关系分解成了View -> Action -> Store -> Model
,每个模块只负责一件事情,数据始终沿着这条链路单向传递。
优势:
在处理大量状态的时候单向数据流更加容易维护,全部事件都经过惟一的入口dispatch
手动触发,数据的每个处理过程都是透明的,这样就能够追踪到每一次的状态变动操做。在前端中Redux的配套工具redux-devtools就提供了一个名为Time Travel
的功能,可以回溯应用的任意历史状态。
全局Store有利于在多个组件之间共享状态。
缺点:
首先Redux为它的数据流指定了大量的规则,无疑会带来更高的学习成本。
在Redux的核心模型中并无考虑异步(Reducer是纯函数),因此如网络请求这样的异步任务还须要经过ActionCreator
之类的机制间接处理,进一步提高了复杂度。
另外一个被广为诟病的缺点是,Redux会引入大量样板代码,在上面这个简单的例子中咱们须要为页面建立Store、State、Reducer、Action等不一样的结构:
即使是修改一个状态变量这样简单的操做都须要通过这一套流程,这无疑会大大增长代码量。
综上所述,Redux模式虽然有许多优势,但它带来的成本也没法忽视。若是你的页面和交互极其复杂或是多个页面之间有大量的共享状态的话能够考虑Redux,可是对于大部分应用来讲,Redux模式并不太适用。
Vue
也是近年来十分热门的前端框架之一,Vuex
则是其专门为Vue
提出的状态管理模式,在Redux之上进行了一些优化;而ReactorKit
是一个Swift的开源库,它的一些设计理念与Vuex十分类似,因此这里我将它们放在一块儿来说。
与ReSwift
不一样的是ReactorKit
的实现自己便于基于RxSwift
,因此没必要再考虑如何与Rx结合,下面是ReactorKit
中数据的流程图:
大致流程与Redux相似,不一样的是Store
变成了Reactor
,这是ReactorKit
引入的一个新概念,它不要求在全局范围统一管理状态,而是每一个组件管理各自的状态,因此每一个视图组件都有各自所对应的Reactor
。
具体的代码请看Demo中的ReactorKit
文件夹,各个部分的含义以下:
Reactor:
如今用ReactorKit
来重写上面的那个例子,首先须要为这个页面建立一个实现了Reactor
协议的类型MessageReactor
:
class MessageReactor: Reactor {
// 与Redux中的Action做用相同,能够是异步
enum Action {
case request
case removeItem(Int)
}
// 表示修改状态的动做(同步)
enum Mutation {
case setMessageList([MsgItem])
case removeItem(Int)
}
// 状态
struct State {
var newsList: [MsgItem] = []
}
...
}
复制代码
一个Reactor须要定义State
、Action
、Mutation
这三个部分,后面会一一介绍。
首先比起Redux这里多了一个Mutation
的概念,在Redux中因为Action直接与Reducer中的操做对应,因此Action只能用来表示同步的操做。ReactorKit
将这个概念更加细化,拆分红了两个部分:Action
和Mutation
:
Action
:视图层触发的动做,能够表示同步和异步(好比网络请求),它最终会被转换成Mutation再被传递到Reducer中;Mutation
:只能表示同步操做,至关于Redux模式中的Action,最终被传入Reducer中参与新状态的计算;mutate():
mutate()
是Reactor中的一个方法,用来将用户触发的Action
转换成Mutation
,mutate()
的存在使得Action能够表示异步操做,由于不管是异步仍是同步的Action最后都会被转换成同步的Mutation:
func mutate(action: MessageReactor.Action) -> Observable<MessageReactor.Mutation> {
switch action {
case .request:
// 1. 异步:网络请求结束后将获得的数据转换成Mutation
return service.request().map { Mutation.setMessageList($0) }
case .removeItem(let index):
// 2. 同步:直接用just包装一个Mutation
return .just(Mutation.removeItem(index))
}
}
复制代码
值得一提的是,这里的mutate()
方法返回的是一个Observable<Mutation>
类型的实例,得益于Rx强大的描述能力,咱们能够用一致的方式来处理同步和异步代码。
reduce():
reduce()
方法这里就没太多可说的了,它扮演的角色与Redux中的Reducer同样,惟一不一样的是这里接受的是一个Mutation
类型,但本质是同样的:
func reduce(state: MessageReactor.State, mutation: MessageReactor.Mutation) -> MessageReactor.State {
var state = state
switch mutation {
case .setMessageList(let news):
state.newsList = news
case .removeItem(let index):
state.newsList.remove(at: index)
}
return state
}
复制代码
Service
图中还有一个与mutate()
产生交互的Service
对象,Service指的是实现具体业务逻辑的地方,Reactor
会经过各个Service
对象来执行具体的业务逻辑,好比说网络请求:
protocol MessageServiceType {
/// 网络请求
func request() -> Observable<[MsgItem]>
}
final class MessageService: MessageServiceType {
func request() -> Observable<[MsgItem]> {
return MessageProvider
.rx
.request(.news)
.mapModel([MsgItem].self)
.asObservable()
}
}
复制代码
看到这里Reactor
的本质基本上已经明了:Reactor
其实是一个中间层,它负责管理视图的状态,并做为视图和具体业务逻辑之间通信的桥梁。
此外ReactorKit
但愿咱们的全部代码都经过函数响应式(FRP)的风格来编写,这从它的API设计上能够看出:Reactor
类型中没有提供如dispatch
这样的方法,而是只提供了一个Subject
类型的变量action
:
var action: ActionSubject<Action> { get }
复制代码
在Rx中Subject
既是观察者又是可观察对象,经常扮演一个中间桥梁的角色。视图上全部的Action
都经过Rx绑定到action
变量上,而不是经过手动触发的方式:比方说咱们想在viewDidLoad
的时候发起一个网络请求,常规的写法是这样的:
override func viewDidLoad() {
super.viewDidLoad()
service.request() // 手动触发一个网络请求动做
}
复制代码
而ReactorKit
所推崇的函数式风格是这样的:
// bind是统一进行事件绑定的地方
func bind(reactor: MessageReactor) {
self.rx.viewDidLoad // 1. 将viewDidLoad做为一个可观察的事件
.map { Reactor.Action.request } // 2. 将viewDidLoad事件转成Action
.bind(to: reactor.action) // 3. 绑定到action变量上
.disposed(by: self.disposeBag)
// ...
}
复制代码
bind
方法是视图层进行事件绑定的地方,咱们将VC的viewDidLoad
做为一个事件源,将其转换成网络请求的Action以后绑定到reactor.action
上,这样当VC的viewDidLoad被调用时该事件源就会发出一个事件并触发Reactor
中网络请求的操做。
这样的写法是更加FRP,一切都是事件流,可是实际用起来并非那么完美。首先咱们须要为用到的全部UI组件提供Rx扩展(上面的例子使用了RxViewController这个库);其次这对reactor实例初始化的时机有更加严格的要求,由于bind
方法是在reactor实例初始化的时候自动调用的,因此不能在viewDidLoad
中初始化,不然会错过viewDidLoad
事件。
RxSwfit
很好的结合在了一块儿,能提供较为完善的函数响应式(FRP)开发体验;Redux模式对于大部分应用来讲仍是过于沉重了,并且Swift的语言特性也不像JavaScript那样灵活,不少样板代码没法避免。因此这里总结了另外一套简化的方案,但愿能在享受单向数据流优点的同时减轻使用者的负担。
详细的代码请看Demo中的Custom
文件夹:
实现很是简单,核心是一个Store
类型:
public protocol StateType { }
public class Store<ConcreteState>: StoreType where ConcreteState: StateType {
public typealias State = ConcreteState
/// 状态变量,一个只读类型的变量
public private(set) var state: State
/// 状态变量对应的可观察对象,当状态发生改变时`rxState`会发送相应的事件
public var rxState: Observable<State> {
return _state.asObservable()
}
/// 强制更新状态,全部的观察者都会收到next事件
public func forceUpdateState() {
_state.onNext(state)
}
/// 在一个闭包中更新状态变量,当闭包返回后一次性应用全部的更新,用于更新状态变量
public func performStateUpdate(_ updater: (inout State) -> Void) {
updater(&self.state)
forceUpdateState()
}
...
}
复制代码
其中StateType
是一个空协议,仅做为类型约束用;Store
做为一个基类,负责保存组件的状态,以及管理状态更新的数据源,核心代码很是简单,下面来看一下实际应用。
在实际开发中我让ViewModel
来处理状态管理和变动的逻辑,再来实现一次上面的那个例子,将一个业务方的ViewModel
分红三个部分:
// <1>
struct MessageState: StateType {
...
}
// <2>
extension Reactive where Base: MessageViewModel {
...
}
// <3>
class MessageViewModel: Store<MessageState> {
required public init(state: MessageState) {
super.init(state: state)
}
...
}
复制代码
各个部分的含义以下:
定义页面的状态变量
描述一个页面所需的全部状态变量都须要定义在一个单独的实现了StateType
协议的struct
中:
struct MessageState: StateType {
var msgList: [MsgItem] = [] // 原始数据
}
复制代码
从前面的代码中能够看到Store
中有一个只读的state
属性:
public private(set) var state: State
复制代码
业务方的ViewModel直接经过self.state
来访问当前的状态变量。而修改状态变量则经过一个performStateUpdate
方法来完成,方法签名以下:
public func performStateUpdate(_ updater: (inout State) -> Void)
复制代码
ViewModel在修改状态变量的时候经过updater
闭包中的参数直接进行修改:
performStateUpdate { $0.msgList = [...] } // 修改状态变量
复制代码
执行完毕后页面的状态会被更新,所绑定的UI组件也会接受到状态更新的事件。这样一来能避免为每个状态变量建立一个Action,简化了流程,同时全部更新状态的操做都由通过同一个入口,有利于以后的分析。
统一管理状态变量有如下几个优势:
- *逻辑清晰:*在浏览页面的代码时只要查看这个类型就能知道哪些变量是须要特别关注的;
- *页面持久化:*只需序列化这个结构体就可以保存这个页面的所有信息,在恢复时只须要将反序列化出来的State赋值给
ViewModel
的state
变量便可:self.state = localState
;
定义对外暴露的可观察变量(Getter)
ViewModel须要暴露一些能让视图进行绑定的可观察对象(Observable),Store
中提供了一个名为rxState
的Observable<State>
类型对象做为状态更新的统一事件源,可是为了更加便于视图层使用,咱们须要将其进一步细化。
这部分逻辑定义在ViewModel的Rx扩展
中,对外提供可观察的属性,这里定义了视图层须要绑定的全部状态。这部分的做用至关于Getter
,是视图层从ViewModel中获取数据源的接口:
extension Reactive where Base: MessageViewModel {
var sections: Observable<[MessageTableSectionModel]> {
return base
.rxState // 从统一的事件源rxState中分流
.map({ (state) -> [MessageTableSectionModel] in
// 将VM中的后端原始模型类型转换成UI层能够直接使用的视图模型
return [
MessageTableSectionModel(items: state.msgList.map { MessageTableCellModel.news($0) })
]
})
}
}
复制代码
这样一来视图层不须要关心State
中的数据类型,直接经过rx
属性来获取本身须要观察的属性便可:
// 视图层直接观察sections,不须要关心内部的转换逻辑
vm.rx.sections.subscribe(...)
复制代码
为何要将视图层使用的接口定义在扩展中,而不是直接观察基类中的
rxState
:
- 定义在Rx扩展中的变量能够直接经过ViewModel的rx属性访问到,便于视图层使用;
- State中的原始数据可能须要必定转换才能让视图层使用(好比上面将原始的
MsgItem
类型转换成TableView能够直接使用的SectionModel模型),这部分的逻辑适合放在扩展的计算属性中,让视图层更加纯粹;
对外提供的方法(Action)
ViewModel还须要接收视图层的事件以触发具体的业务逻辑,若是这一步经过Rx绑定的方式来完成的话,会对业务层代码的编写方式带来不少限制(参考上面的ReactorKit)。因此这部分不作过多的封装,仍是经过方法的形式来对外暴露接口,这部分就至关于Action,不过这样的代价是Action没法再经过统一的接口来派发:
class MessageViewModel: Store<MessageState> {
// 请求
func request() {
state.loadingState = .loading
MessageProvider.rx
.request(.news)
.map([MsgItem].self)
.subscribe(onSuccess: { (items) in
// 请求完成后改变state中响应的变量,UI层会自动响应
self.performStateUpdate {
$0.msgList = items
$0.loadingState = .normal
}
}, onError: { error in
self.performStateUpdate { $0.loadingState = .normal }
})
.disposed(by: self.disposeBag)
}
}
复制代码
咱们以前已经将状态和UI彻底分离开来了,因此在ViewModel的逻辑中只须要关心state
中的状态便可,不须要关心与视图层的交互,因此以这种方式编写的代码一样也是十分清晰的。
视图层须要实现一个名为View
的协议,这里主要参考了ReactorKit
中的设计:
/// 视图层协议
public protocol View: class {
/// 用于声明该视图对应的ViewModel的类型
associatedtype ViewModel: StoreType
/// ViewModel的实例,有默认实现,视图层须要在合适的时机初始化
var viewModel: ViewModel? { set get }
/// 视图层实现这个方法,并在其中进行绑定
func doBinding(_ vm: ViewModel)
}
复制代码
对于视图层来讲,它须要作两件事:
实现一个doBinding
方法,全部的Rx事件绑定都放在这个方法中完成:
func doBinding(_ vm: MessageViewModel) {
vm.rx.sections
.drive(self.tableView.rx.items(dataSource: dataSource))
.disposed(by: self.disposeBag)
}
复制代码
在合适的时机初始化viewModel
属性:
override func viewDidLoad() {
super.viewDidLoad()
// 初始化ViewModel
self.viewModel = MessageViewModel(state: MessageState())
}
复制代码
当viewModel
初始化完成后会自动调用doBinding
方法进行绑定,而且在实例的生命周期中只会被执行一次。
在视图层中对于各类状态的绑定是很重要的一个环节,View
协议存在的意义在于将视图层的事件绑定规范化,防止绑定操做的代码散落在各处下降可读性。
按照以上流程实现的页面数据流以下:
ViewModel
中的逻辑;ViewModel
中执行具体的业务逻辑,并经过performStateUpdate
修改保存在State中的状态变量;这样能保证一个页面的数据始终按照预期的方式来变化,并且单向数据流的特色使得咱们能够像Redux这样追踪全部状态的变动,好比说咱们能够简单的利用Swift的反射(Mirror
)来将全部状态的变动打印到控制台中:
public func performStateUpdate(_ updater: (inout State) -> Void) {
updater(&self.state)
#if DEBUG
StateChangeRecorder.shared.record(state, on: self) // 记录状态的变动
#endif
forceUpdateState()
}
复制代码
实现的代码在StateChangeRecorder.swift
文件中,很是简单只有不到100行。每当有状态发生改变的时候就会在控制台中打印一条Log:
若是你为全部StateType
类型实现序列化和反序列化的操做,甚至能够实现相似redux-devtools这样的Time Travel
功能,这里就再也不继续引伸了。
引入Rx模式须要多方面的考虑,本文仅针对状态管理这一点做了介绍,上面介绍的三种方案各有特色,最终的选择仍是要结合项目的实际状况来判断。