1.引言
之前学习React
期间接触到了Redux
,第一次对基于状态管理的单向数据流架构有了一个认识,简单的公式即UI = f(state)
。下面是Redux的一个架构图(看不懂,就继续往下看):
单项数据流架构
如下:
React
中的React-Redux- ReactorKit
- ReSwift
这里只是抛砖引玉,本文主要以ReactorKit
和ReSwift
为主进行一个介绍,让我门在写代码的时候,思想上能够打破以前iOS开发中的固有思维
2.为什么会出现单向数据流架构
MVC
,MVVM
,MVP
、VIPER
这些架构设计方案,广义上讲都是MVC
,本质目的都是从ViewController中把逻辑拆分出去,对控制器进行瘦身和更细粒度的职责划分.比如
MVVM
: 把View
数据展示的逻辑、业务逻辑的处理,页面状态的描述独立出来形成ViewModel
模块,架构风格就变成了MVVM
VIPER
: 把路由的职责,获取数据的职责也独立出去,架构风格就变成了VIPER
❓❓但是它们都没办法很好解决以下几个问题
- 复杂界面设计的实现和样式管理
- 应用的状态分散 难以管理
- View Controller 难以测试
- 如何管理Action对应用状态的影响
- 数据的变化应该只和用户输入有关
正是基于此,出现了基于状态管理的单向数据流架构
:Redux、ReactorKit、ReSwift
3.什么是单向数据流架构
3.1 特点
可以说满足以下几个特点的,就可以说它是单向数据流架构
- 统一管理应用状态,包括统一的机制和唯一的状态容器,这让应用状态的改变更容易预测,也更容易调试。
- 清晰的逻辑拆分,清晰的代码组织方式,让团队的协作更加容易。
- 函数式的编程方式,每个组件都只做一件小事并且是独立的小函数,这增加了应用的可测试性。
- 单向数据流,数据驱动UI的编程方式,特别重要的一个点。
3.2 架构图
ReactorKit
架构图如下:
Action 代表用户行为
,State
代表页面状态。Mutation
是Action
和State
的桥梁。Reactor
通过两步将用户行为序列转换为页面状态序列:mutate()
和reduce()
ReSwift架构图
如下:
store
: 用来存储整个应用的state
state
: 一个用来描述应用状态的对象action
:一个对象,用来描述程序发生的动作reduce
: 是更改state
的纯函数,接受state
和action
参数最后返回新的state
可以看出ReSwift
和ReactorKit
架构图基本上是一样的,主要的不同在于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,最主要是能够了解到基于状态管理的单向数据流架构的思想,只要存在状态变化,这套方式可以在任何地方适用。
参考文章: