在这个 计算器的 Demo 中,演示了状态机类型场景如何处理。
分为内部和外部,内部是指状态机,外部是指特殊的场景。
如果把状态机想象成一个黑盒的话,它跟外部的交互分为两种:
- 外部触发状态机改变状态,我们先管它叫反馈给状态机
- 外部监听状态机的状态,我们先管它叫订阅状态机
状态机
状态机需要知道:
- 初始的状态
- 新状态是如何根据当前状态和事件计算出来的:新状态 = 当前状态 + 事件
- 在什么线程执行新状态的获取过程
- UI 如何和状态交互
比如在计算器场景中,UI与状态机的交互有:
- 点击按钮改变状态机的状态
- 计算结果label 需要监听状态机的状态以展示计算结果
代码如下:
Observable.system(
initialState: CalculatorState.initial,
reduce: CalculatorState.reduce,
scheduler: MainScheduler.instance,
scheduledFeedback: uiFeedback
)
.subscribe()
.disposed(by: disposeBag)
状态机内部是怎么实现的(内部实现嵌套层次多,导致代码不太好看,我只取重点代码):
let replaySubject = ReplaySubject<State>.create(bufferSize: 1)
let asyncScheduler = scheduler.async
let events: Observable<Event> = Observable.merge(scheduledFeedback.map { feedback in
let state = ObservableSchedulerContext(source: replaySubject.asObservable(), scheduler: asyncScheduler)
return feedback(state)
})
.observeOn(CurrentThreadScheduler.instance)
return events.scan(initialState, accumulator: reduce)
.do(onNext: { output in
replaySubject.onNext(output)
}, onSubscribed: {
replaySubject.onNext(initialState)
})
.subscribeOn(scheduler)
.startWith(initialState)
.observeOn(scheduler)
1. 首先是组合scheduledFeedback
scheduledFeedback是个闭包数组。由于外部需要定义 反馈给状态机 和 订阅状态机,所以该闭包需要提供以下参数:
- 以状态作为 Event 的源:订阅状态机需要的,也就是
ReplaySubject<State>
- scheduler:这个是我这篇博客的bug,我暂时还不知道为啥要提供这个,😆
scheduledFeedback 闭包会返回一个 以状态机的事件类型作为 Event 的源,在计算器的例子中也就是 Observable<CalculatorCommand>
events 是把外部定义的多个源组合而成的一个源。
2. 与外部定义的源交互
- 订阅外部定义的源们:
events.scan(initialState, accumulator: reduce)
, - 一旦状态发生改变,通过告知 replaySubject 来通知到外部:
.do(onNext: { output in
replaySubject.onNext(output)
}, onSubscribed: {
replaySubject.onNext(initialState)
})
外部如何定义源的
let uiFeedback: FeedbackLoop = bind(self) { this, state in
let subscriptions = [
state.map { $0.screen }.bind(to: this.resultLabel.rx.text),
state.map { $0.sign }.bind(to: this.lastSignLabel.rx.text)
]
let events: [Observable<CalculatorCommand>] = [
this.allClearButton.rx.tap.map { _ in .clear },
this.zeroButton.rx.tap.map { _ in .addNumber("0") },
// 等等,没有列出来完
]
return Bindings(subscriptions: subscriptions, events: events)
}
- subscriptions 是定义如何订阅状态机的
- events 是定义如何把按钮的点击事件转换成状态机需要的 CalculatorCommand 源
把 subscriptions 和 events 封装在 Bindings,我认为主要是为了 bind 方法方便。
下面看看 bind 方法里做了什么?
public func bind<State, Event>(_ bindings: @escaping (ObservableSchedulerContext<State>) -> (Bindings<Event>)) -> (ObservableSchedulerContext<State>) -> Observable<Event> {
return { (state: ObservableSchedulerContext<State>) -> Observable<Event> in
return Observable<Event>.using({ () -> Bindings<Event> in
return bindings(state)
}, observableFactory: { (bindings: Bindings<Event>) -> Observable<Event> in
return Observable<Event>.merge(bindings.events)
.enqueue(state.scheduler)
})
}
}
看起来好像只是把 一堆 events 转成了一个源,我不太确定,因为感觉跟方法名没关系😹
GitHubSignupViewController1 用这种方式怎么做
GitHubSignupViewController1 的需求:
- 输入名字 -> 通过网络验证名字的可用性 -> 可用性结果反馈在 label 上
- 输入密码 -> 本地验证密码的是否符合要求 -> 结果反馈在 label 上
- 再次输入密码 -> 本地验证再次输入的密码是否一致 -> 结果反馈在 label 上
- 以上3条都正确时 -> 注册按钮可用
- 点击注册 -> 发起网络请求,正在注册的状态,显示 loading
- 注册成功/失败 -> 弹窗提示
因为这个需求不像计算器有非常清晰的状态,所以没办法用状态机。
但是如果把 1、2、3 改成依赖关系:必须名字可用之后,才能输入密码,必须密码符合要求之后,才能输入再次密码。这样就有这样的状态: 初始 -> 输入名字 -> 输入密码 -> 再次输入密码 -> 准备好注册 -> 注册中 -> 注册成功/注册失败
这样就可以用状态机了。