谈谈RxSwift和状态管理

前段时间在RxSwift上作了一些实践,Rx确实是一个强大的工具,但同时也是一把双刃剑,若是滥用的话反而会带来反作用,本文就引入Rx模式以后如何更好的管理应用的状态和逻辑作了一些粗浅的总结。前端

本文篇幅较长,主要围绕着状态管理这一话题进行介绍,前两个部分介绍了前端领域中React和Vue所采用的状态管理模式及其在Swift中的实现,最后介绍了另外一种简化的状态管理方案。不会涉及复杂的Rx特性,阅读前对Rx有一些基本的了解便可。react

为何状态管理这么重要

一个复杂的页面一般须要维护大量的变量来表示其运行期间的各类状态,在MVVM中页面大部分的状态和逻辑都经过ViewModel来维护,在常见的写法中ViewModel和视图之间一般用Delegate来通信,好比说在数据改变的时候通知视图层更新UI等等:git

MVVM

在这种模式中,ViewModel的状态更新以后须要咱们调用Delegate手动通知视图层。而在Rx中这一层关系被淡化了,因为Rx是响应式的,设定好绑定关系后ViewModel只须要改变数据的值,Rx会自动的通知每个观察者:github

Rx

Rx为咱们隐藏了通知视图的过程,首先这样的好处是明显的:ViewModel能够更加专一于数据自己,不用再去管UI层的逻辑;可是滥用这个特性也会带来麻烦,大量的可观察变量和绑定操做会让逻辑变得含糊不清,修改一个变量的时候可能会致使一系列难以预料的连锁反应,这样代码反而会变得更加难以维护。编程

想要更好的过渡到响应式编程,一个统一的状态管理方案是不可或缺的。在这一块前端领域有很多成熟的实践方案,Swift中也有一些开源库对其进行了实现,其中的思想咱们能够先来参考一下。redux

下面的介绍中所涉及的示例代码在:github.com/L-Zephyr/My…swift

Redux - ReSwift

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中所描述的数据流:

redux

其中的State就是应用的状态,也就是咱们的Model部分,先无论这里的ActionReducer等概念,从图中能够看到State和View是有着直接的绑定关系的,而View的事件则会经过ActionStore等一系列操做间接的改变State,下面来详细的介绍一下Redux的数据流的实现以及所涉及到的概念:

  1. View 顾名思义,View就是视图,用户在视图上的操做事件不会直接修改模型,而是会被映射成一个个Action

  2. 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所携带的数据保存在其成员变量中。

  3. 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))
    复制代码
  4. 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模式并不太适用。

Vuex - ReactorKit

Vue也是近年来十分热门的前端框架之一,Vuex则是其专门为Vue提出的状态管理模式,在Redux之上进行了一些优化;而ReactorKit是一个Swift的开源库,它的一些设计理念与Vuex十分类似,因此这里我将它们放在一块儿来说。

实现

ReSwift不一样的是ReactorKit的实现自己便于基于RxSwift,因此没必要再考虑如何与Rx结合,下面是ReactorKit中数据的流程图:

Reactor

大致流程与Redux相似,不一样的是Store变成了Reactor,这是ReactorKit引入的一个新概念,它不要求在全局范围统一管理状态,而是每一个组件管理各自的状态,因此每一个视图组件都有各自所对应的Reactor

具体的代码请看Demo中的ReactorKit文件夹,各个部分的含义以下:

  1. 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须要定义StateActionMutation这三个部分,后面会一一介绍。

    首先比起Redux这里多了一个Mutation的概念,在Redux中因为Action直接与Reducer中的操做对应,因此Action只能用来表示同步的操做。ReactorKit将这个概念更加细化,拆分红了两个部分:ActionMutation

    • Action:视图层触发的动做,能够表示同步和异步(好比网络请求),它最终会被转换成Mutation再被传递到Reducer中;
    • Mutation:只能表示同步操做,至关于Redux模式中的Action,最终被传入Reducer中参与新状态的计算;
  2. mutate():

    mutate()是Reactor中的一个方法,用来将用户触发的Action转换成Mutationmutate()的存在使得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强大的描述能力,咱们能够用一致的方式来处理同步和异步代码。

  3. 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
    }
    复制代码
  4. 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事件。

分析

  • 优势
    • 相比ReSwift简化了一些流程,而且以组件为单位来管理各自的状态,相比起来更容易在现有工程中引入;
    • RxSwfit很好的结合在了一块儿,能提供较为完善的函数响应式(FRP)开发体验;
  • 缺点
    • 由于核心思想仍是Redux模式,因此模板代码过多的问题仍是没法避免;

另外一种简化方案

Redux模式对于大部分应用来讲仍是过于沉重了,并且Swift的语言特性也不像JavaScript那样灵活,不少样板代码没法避免。因此这里总结了另外一套简化的方案,但愿能在享受单向数据流优点的同时减轻使用者的负担。

详细的代码请看Demo中的Custom文件夹:

Demo

实现很是简单,核心是一个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来处理状态管理和变动的逻辑,再来实现一次上面的那个例子,将一个业务方的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赋值给ViewModelstate变量便可:self.state = localState
    • *便于测试:*单元测试时能够经过检查State类型的变量来进行测试;
  • 定义对外暴露的可观察变量(Getter)

    ViewModel须要暴露一些能让视图进行绑定的可观察对象(Observable),Store中提供了一个名为rxStateObservable<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

视图层须要实现一个名为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协议存在的意义在于将视图层的事件绑定规范化,防止绑定操做的代码散落在各处下降可读性。

数据流

按照以上流程实现的页面数据流以下:

数据流

  1. 视图(View)中的事件触发时,直接调用相应的方法触发ViewModel中的逻辑;
  2. ViewModel中执行具体的业务逻辑,并经过performStateUpdate修改保存在State中的状态变量;
  3. 状态变量发生改变以后,经过Rx的绑定自动通知视图层更新UI;

这样能保证一个页面的数据始终按照预期的方式来变化,并且单向数据流的特色使得咱们能够像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:

States Change Log

若是你为全部StateType类型实现序列化和反序列化的操做,甚至能够实现相似redux-devtools这样的Time Travel功能,这里就再也不继续引伸了。

总结

引入Rx模式须要多方面的考虑,本文仅针对状态管理这一点做了介绍,上面介绍的三种方案各有特色,最终的选择仍是要结合项目的实际状况来判断。

相关文章
相关标签/搜索