架构系列—基于状态管理的单向数据流架构

4,529 阅读6分钟

1.引言

之前学习React期间接触到了Redux,第一次对基于状态管理的单向数据流架构有了一个认识,简单的公式即UI = f(state)。下面是Redux的一个架构图(看不懂,就继续往下看):

Redux其实就是一种单向数据流架构,衍生到iOS中,目前个人接触的几种单项数据流架构如下:

这里只是抛砖引玉,本文主要以ReactorKitReSwift为主进行一个介绍,让我门在写代码的时候,思想上能够打破以前iOS开发中的固有思维

2.为什么会出现单向数据流架构

MVCMVVMMVPVIPER这些架构设计方案,广义上讲都是MVC,本质目的都是从ViewController中把逻辑拆分出去,对控制器进行瘦身和更细粒度的职责划分.比如

  • MVVM: 把View数据展示的逻辑、业务逻辑的处理,页面状态的描述独立出来形成ViewModel模块,架构风格就变成了MVVM
  • VIPER: 把路由的职责,获取数据的职责也独立出去,架构风格就变成了VIPER

❓❓但是它们都没办法很好解决以下几个问题

  • 复杂界面设计的实现和样式管理
  • 应用的状态分散 难以管理
  • View Controller 难以测试
  • 如何管理Action对应用状态的影响
  • 数据的变化应该只和用户输入有关

正是基于此,出现了基于状态管理的单向数据流架构:Redux、ReactorKit、ReSwift

3.什么是单向数据流架构

3.1 特点

可以说满足以下几个特点的,就可以说它是单向数据流架构

  • 统一管理应用状态,包括统一的机制和唯一的状态容器,这让应用状态的改变更容易预测,也更容易调试。
  • 清晰的逻辑拆分,清晰的代码组织方式,让团队的协作更加容易。
  • 函数式的编程方式,每个组件都只做一件小事并且是独立的小函数,这增加了应用的可测试性。
  • 单向数据流,数据驱动UI的编程方式,特别重要的一个点。

3.2 架构图

ReactorKit架构图如下:

  • Action 代表用户行为
  • State 代表页面状态。
  • MutationActionState 的桥梁。
  • Reactor 通过两步将用户行为序列转换为页面状态序列:mutate()reduce()

ReSwift架构图如下:

  • store: 用来存储整个应用的 state
  • state: 一个用来描述应用状态的对象
  • action:一个对象,用来描述程序发生的动作
  • reduce: 是更改state的纯函数,接受 stateaction参数最后返回新的state

可以看出ReSwiftReactorKit架构图基本上是一样的,主要的不同在于Store和Reactor的内部处理上会有一些偏差

3.3 工作流程

不管是哪一种架构,本质思想其实和Redux都是一样的、工作流程都是类似的,在这个机制下, 一个App的状态转换如下:

  • 初始化State -> 初始化UI,并把它绑定到对应的State的属性上
  • 业务操作 -> 产生Action -> Reducer接收Action和当State产生新的State
  • 更新当前State -> 通知UI State有更新 -> UI显示新的状态 -> 下一个业务操作...

将整个应用的state存储到一个store/reactor中,当用户触发一个action的时候,通过调用store.dispatch(action)来触发sotre中定义好的reduce,最终更新引用的state

4. 项目中如何使用

考虑到篇幅原因,这里以ReactorKit来说明【单项数据流架构】在项目中的大概用法

更具体的用法可以参考ReactorKit使用入门

实际使用中更推荐ReSwift,更贴近Redux一些

4.1 View

View职责

  • 将用户输入绑定到 Action 的序列上
  • 同时将页面状态绑定到 UI 组件上
class ProfileViewController: UIViewController, View {
  func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}
}
  • 外部初始化代码

reactor 属性被设置时,bind(reactor:) 方法就会被调用

let profileViewController = ProfileViewController()
profileViewController.reactor = UserViewReactor()

4.2 Reactor

每一个 View 都有对应的 Reactor 并且将所有的逻辑代理给 Reactor,它具有以下几个特点:

  • Reactor 是与 UI 相互独立的一层,主要负责状态管理
  • 业务逻辑从 View 中被抽离到了Reactor
  • Reactor 不需要依赖 View,所以它很容易被测试

具体使用自定义一个Reactor遵循 Reactor协议即可

class ProfileViewReactor: Reactor {
  // 代表用户行为
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // 代表附加作用
  enum Mutation {
    case setFollowing(Bool)
  }

  // 代表页面状态
  struct State {
    var isFollowing: Bool = false
  }
  let initialState: State = State()
}
  • mutate()

个人把mutate理解为对action异步操作做了一层封装而已,这样就比较好理解了

mutate()接收一个 Action ,然后创建一个 Observable<Mutation>

func mutate(action: Action) -> Observable<Mutation>

如异步操作,API 调用都是在这个方法内执行

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive an action
    return UserAPI.isFollowing(userID) // create an API stream
      .map { (isFollowing: Bool) -> Mutation in
        return Mutation.setFollowing(isFollowing) // convert to Mutation stream
      }
}
  • reduce()

reduce() 通过旧的 State 以及 Mutation 创建一个新的 State

reduce 有一个很重要的特点,它是纯函数将同步的返回一个 State。不会产生其他的副作用

func reduce(state: State, mutation: Mutation) -> State {
  var state = state // create a copy of the old state
  switch mutation {
  case let .setFollowing(isFollowing):
    state.isFollowing = isFollowing // manipulate the state, creating a new state
    return state // return the new state
  }
}

5. 再聊聊单向数据流

任何新的状态其实都是在原有状态的基础上通过一些改变所得到的,我门可以把它抽象为一个公式

新状态 = f(旧状态, 用户行为)

对应到我门的reduce,使用swift表达出来就是:

func reduce(state: State, userAction: Action) -> State

它接受一个已有状态State和一个输入 Action,将 Action 作用于 state,并给出新的 State

有了reduce(state: State, userAction: Action),我们就可以将用户操作抽象为 Action,并且将所有的状态更新集中处理了(Store),Store 来存储状态,并通过向 Store 发送 Action 来更新其中的状态

接收到状态更新的对象(比如ViewController)可以订阅状态变化,以更新 UI。订阅者不参与直接改变状态,而只是发送可能改变状态的行为,然后接受状态变化并更新 UI,以此形成单向的数据流动。而因为更新 UI 的代码将会是纯函数的,所以 ViewController 的 UI 也将是可测试的

6. 总结

不管是Redux、ReactorKit还是ReSwift,最主要是能够了解到基于状态管理的单向数据流架构的思想,只要存在状态变化,这套方式可以在任何地方适用。

参考文章: