Swift 的单向数据流状态管理架构 - 受 Vuex 和 Flux 启发
简介
VueFlux 是 Swift 的单向数据流状态管理架构,受 Vuex 和 Flux 启发。
它支持多仓库,因此所有 ViewControllers 都有指定的仓库,并且有规则确保状态只能以可预测的方式更改。
仓库还可以接收全局派发的操作。
这使得 ViewControllers 之间无需依赖。应用程序中的共享状态也通过仓库的共享实例支持。
尽管 VueFlux 使您的项目更具生产力和代码更易读,但它也带来了更多概念和样板文件的代价。
如果您的项目是小型项目,那么您很可能会没有 VueFlux 也能正常工作。
然而,随着您项目规模的扩大,VueFlux 将是处理复杂数据流的最佳选择。
VueFlux 通过高效的响应系统接收状态更改。 VueFluxReactive 是与该架构兼容的 µ 响应式框架。
任意第三方响应式框架(例如 RxSwift、ReactiveSwift 等)也可以与 VueFlux 一起使用。
关于 VueFlux
VueFlux通过明确划分ViewController的组成角色,创建了一种单向且可预测的流。它由以下核心概念构成。
ViewController使用响应式系统来观察状态变化。
示例代码使用VueFluxReactive,将在后面进行描述。
您可以在这里查看示例实现。
状态
这是一个仅用于约束行为和突变的协议,表示存储管理的状态。
实现状态的某些属性,并使用fileprivate访问控制保持它们为只读,如下所示。
只能通过突变进行突变,并通过计算属性发布属性。
final class CounterState: State {
typealias Action = CounterAction
typealias Mutations = CounterMutations
fileprivate let count = Variable(0)
}
行为
这是分发行为的函数的代理。
它们可以具有任意操作,如异步地对后端API进行请求。
从行为函数分发的行为的类型由状态确定。
enum CounterAction {
case increment, decrement
}
extension Actions where State == CounterState {
func increment() {
dispatch(action: .increment)
}
func decrement() {
dispatch(action: .decrement)
}
}
突变
这是一个表示突变状态的commit
函数的协议。
通过在相同文件中实现它,可以更改fileprivate属性的状态。
在存储中实际更改状态的唯一方法是通过对突变提交行为来实现。
状态的变化必须同步进行。
struct CounterMutations: Mutations {
func commit(action: CounterAction, state: CounterState) {
switch action {
case .increment:
state.count.value += 1
case .decrement:
state.count.value -= 1
}
}
}
计算属性
这是发布状态只读属性的代理。
通过在相同文件中实现它,可以访问和发布状态的fileprivate属性。
Store中存储的状态属性只能通过这种方式访问。
extension Computed where State == CounterState {
var countTextValues: Signal<String> {
return state.count.signal.map { String($0) }
}
}
仓库
仓库管理状态,也可以通过共享仓库实例管理应用程序中的共享状态。
计算属性和操作只能通过这个来访问。改变状态也是如此。
从实例成员的actions
分发的操作只能修改指定状态的仓库。
另一方面,从静态成员的actions
分发的操作会修改具有相同通用类型State的仓库中管理的所有状态。
在ViewController中的仓库实现如下
final class CounterViewController: UIViewController {
@IBOutlet private weak var counterLabel: UILabel!
private let store = Store<CounterState>(state: .init(), mutations: .init(), executor: .queue(.global()))
override func viewDidLoad() {
super.viewDidLoad()
store.computed.countTextValues.bind(to: counterLabel, \.text)
}
@IBAction func incrementButtonTapped(sender: UIButton) {
store.actions.increment() // Store<CounterState>.actions.increment()
}
@IBAction func decrementButtonTapped(sender: UIButton) {
store.actions.decrement() // Store<CounterState>.actions.decrement()
}
}
关于VueFluxReactive
VueFluxReactive是一个用于观察状态变化的微反应系统。
它是为了替换现有的反应框架,如RxSwift和ReactiveSwift,它们虽然功能强大但学习成本和入门成本很高。
但当然,VueFlux也可以与这些框架一起使用,因为VueFluxReactive是分离的。
VueFluxReactive由以下原始组件构成。
接受器
该类型有一种生成信号的方法。
可以向接受器发送值并通过观察生成的信号接收它。
从接受器生成的信号不保留最新值。
在实际应用中,它用于从State向ViewController发送命令(如显示另一个ViewController)。
不能递归地传递值.
let sink = Sink<Int>()
let signal = sink.signal
signal.observe { print($0) }
sink.send(value: 100)
// prints "100"
信号
一个推力驱动的流,在时间上发送值变化。
值将同时发送到所有注册的观察者。
所有值的变化都是通过这个原始组件进行的。
let sink = Sink<Int>()
let signal = sink.signal
signal.observe { print("1: \($0)") }
signal.observe { print($2: \($0)") }
sink.send(value: 100)
sink.send(value: 200)
// prints "1: 100"
// prints "2: 100"
// prints "1: 200"
// prints "2: 200"
变量
变量表示一个线程安全的可变值,允许通过它生成的信号来观察它的变化。
观察开始时,信号将发送最新值。所有值更改在此之后都会发送。
不能递归地传递值.
let variable = Variable(0)
variable.signal.observe { print($0) }
variable.value = 1
print(variable.value)
variable.signal.observe { print($0) }
/// prints "0"
/// prints "1"
/// prints "1"
/// prints "1"
常量
这是一种用于使变量只读的包装器。
从变量生成的常量反映了其变量的变化。
就像变量一样,最新值和值更改都通过信号转发。但常量不允许直接更改。
let variable = Variable(0)
let constant = variable.constant
constant.signal.observe { print($0) }
variable.value = 1
print(constant.value)
constant.signal.observe { print($0) }
/// prints "0"
/// prints "1"
/// prints "1"
/// prints "1"
高级用法
执行器
执行器确定了函数的执行上下文,例如在主线程上执行、在全局队列上执行等。
一些上下文是默认内置的。
-
立即
立即异步执行函数。 -
mainThread
如果执行线程是主线程,则立即和同步执行。否则,将其排队到主队列。 -
queue(_ dispatchQueue: DispatchQueue)
所有函数都被排队到给定的dispatch队列。
在以下情况下,存储将提交操作到全局队列上的变更。
let store = Store<CounterState>(state: .init(), mutations: .init(), executor: .queue(.global()))
如果您观察到如下情况,观察函数将在全局后台队列上执行。
store.computed.valueSignal
.observe(on: .queue(.global(qos: .background)))
.observe { value in
// Executed on global background queue
}
信号运算符
VueFluxReactive限制了功能性方法AMAP。
然而,包含了一些便利的最小运算符。
这些运算符将信号转换为新信号,由运算符生成,这意味着信号的不变性得到保持。
map
map运算符用于转换信号的值。
let sink = Sink<Int>()
let signal = sink.signal
signal
.map { "Value is \($0)" }
.observe { print($0) }
sink.send(value: 100)
sink.send(value: 200)
// prints "Value is 100"
// prints "Value is 200"
observe(on:)
在给定的执行器上下文中转发所有值。
let sink = Sink<Int>()
let signal = sink.signal
signal
.observe(on: .mainThread)
.observe { print("Value: \($0), isMainThread: \(Thread.isMainThread)") }
DispatchQueue.global().async {
sink.send(value: 100)
sink.send(value: 200)
}
// prints "Value: 100, isMainThread: true"
// prints "Value: 200, isMainThread: true"
可销毁的
可销毁的表示可以销毁的东西,通常注销已注册到信号的观察者。
let disposable = signal.observe { value in
// Not executed after disposed.
}
disposable.dispose()
DisposableScope
DisposableScope是Disposable的资源管理器。
在析构或释放时,这将终止所有添加的Disposable。
例如,当具有DisposableScope属性的所有ViewController被 dismissal 时,所有Disposable将被终止。
var disposableScope: DisposableScope? = DisposableScope()
disposableScope += signal.observe { value in
// Not executed after disposableScope had deinitialized.
}
disposableScope = nil // Be disposed
区域观察
在观察中,您可以将AnyObject
作为duringScopeOf:
的参数。
当对象被析构时,观察Signal的观察函数将被丢弃。
signal.observe(duringScopeOf: self) { value in
// Not executed after `self` had deinitialized.
}
绑定
绑定确保目标对象的值更新为通过Signal接收到的最新值。
目标对象被析构后,绑定不再有效。
绑定默认在主线程上工作。
闭包绑定。
text.signal.bind(to: label) { label, text in
label.text = text
}
智能键路径绑定。
text.signal.bind(to: label, \.text)
Binder
extension UIView {
func setHiddenBinder(duration: TimeInterval) -> Binder<Bool> {
return Binder(target: self) { view, isHidden in
UIView.transition(
with: view,
duration: duration,
options: .transitionCrossDissolve,
animations: { view.isHidden = isHidden }
)
}
}
}
isViewHidden.signal.bind(to: view.setHiddenBinder(duration: 0.3))
共享存储
您应该创建Store的共享实例来管理应用程序中共享的状态。
虽然您可以将它定义为全局变量,但优雅的方法是覆盖Store并定义一个静态成员shared
。
final class CounterStore: Store<CounterState> {
static let shared = CounterStore()
private init() {
super.init(state: .init(), mutations: .init(), executor: .queue(.global()))
}
}
全局分发
VueFlux还可以作为全局事件总线。
如果您从Store的静态成员 actions
调用函数,则影响具有相同State泛型类型的所有Store中的状态。
let store = Store<CounterState>(state: .init(), mutations: .init(), executor: .immediate)
print(store.computed.count.value)
Store<CounterState>.actions.increment()
print(store.computed.count.value)
// prints "0"
// prints "1"
要求
- Swift 4.1+
- macOS 10.9+
- iOS 9.0+
- watchOS 2.0+
- tvOS 9.0+
安装
CocoaPods
如果使用 VueFlux 和 VueFluxReactive,请在您的 Podfile 中添加以下内容
use_frameworks!
target 'TargetName' do
pod 'VueFluxReactive'
end
或者如果使用第三方 Reactive 框架
use_frameworks!
target 'TargetName' do
pod 'VueFlux'
# And reactive framework you like
end
然后运行
pod install
Carthage
在您的 Cartfile 中添加以下内容
github "ra1028/VueFlux"
然后运行
carthage update
示例项目
- VueFluxExample-GitHub:使用 GitHub 用户搜索 API 的简单示例。
贡献
欢迎fork并提交pull request。
在提交pull request之前,请确保您已经通过了包含的测试。
如果您的pull request 包含新功能,请为它编写测试用例。
授权
VueFlux和VueFluxReactive在MIT许可下发布。