前段時間在RxSwift上作了一些實踐,Rx確實是一個強大的工具,但同時也是一把雙刃劍,若是濫用的話反而會帶來反作用,本文就引入Rx模式以後如何更好的管理應用的狀態和邏輯作了一些粗淺的總結。前端
本文篇幅較長,主要圍繞着狀態管理這一話題進行介紹,前兩個部分介紹了前端領域中React和Vue所採用的狀態管理模式及其在Swift中的實現,最後介紹了另外一種簡化的狀態管理方案。不會涉及複雜的Rx特性,閱讀前對Rx有一些基本的瞭解便可。react
一個複雜的頁面一般須要維護大量的變量來表示其運行期間的各類狀態,在MVVM中頁面大部分的狀態和邏輯都經過ViewModel來維護,在常見的寫法中ViewModel和視圖之間一般用Delegate
來通信,好比說在數據改變的時候通知視圖層更新UI等等:git
在這種模式中,ViewModel的狀態更新以後須要咱們調用Delegate手動通知視圖層。而在Rx中這一層關係被淡化了,因爲Rx是響應式的,設定好綁定關係後ViewModel只須要改變數據的值,Rx會自動的通知每個觀察者:github
Rx爲咱們隱藏了通知視圖的過程,首先這樣的好處是明顯的:ViewModel能夠更加專一於數據自己,不用再去管UI層的邏輯;可是濫用這個特性也會帶來麻煩,大量的可觀察變量和綁定操做會讓邏輯變得含糊不清,修改一個變量的時候可能會致使一系列難以預料的連鎖反應,這樣代碼反而會變得更加難以維護。編程
想要更好的過渡到響應式編程,一個統一的狀態管理方案是不可或缺的。在這一塊前端領域有很多成熟的實踐方案,Swift中也有一些開源庫對其進行了實現,其中的思想咱們能夠先來參考一下。redux
下面的介紹中所涉及的示例代碼在:github.com/L-Zephyr/My…。swift
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中所描述的數據流:
其中的State
就是應用的狀態,也就是咱們的Model部分,先無論這裏的Action
、Reducer
等概念,從圖中能夠看到State和View是有着直接的綁定關係的,而View的事件則會經過Action
、Store
等一系列操做間接的改變State
,下面來詳細的介紹一下Redux的數據流的實現以及所涉及到的概念:
View 顧名思義,View就是視圖,用戶在視圖上的操做事件不會直接修改模型,而是會被映射成一個個Action
。
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所攜帶的數據保存在其成員變量中。
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))
複製代碼
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模式並不太適用。
Vue
也是近年來十分熱門的前端框架之一,Vuex
則是其專門爲Vue
提出的狀態管理模式,在Redux之上進行了一些優化;而ReactorKit
是一個Swift的開源庫,它的一些設計理念與Vuex十分類似,因此這裏我將它們放在一塊兒來說。
與ReSwift
不一樣的是ReactorKit
的實現自己便於基於RxSwift
,因此沒必要再考慮如何與Rx結合,下面是ReactorKit
中數據的流程圖:
大致流程與Redux相似,不一樣的是Store
變成了Reactor
,這是ReactorKit
引入的一個新概念,它不要求在全局範圍統一管理狀態,而是每一個組件管理各自的狀態,因此每一個視圖組件都有各自所對應的Reactor
。
具體的代碼請看Demo中的ReactorKit
文件夾,各個部分的含義以下:
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須要定義State
、Action
、Mutation
這三個部分,後面會一一介紹。
首先比起Redux這裏多了一個Mutation
的概念,在Redux中因爲Action直接與Reducer中的操做對應,因此Action只能用來表示同步的操做。ReactorKit
將這個概念更加細化,拆分紅了兩個部分:Action
和Mutation
:
Action
:視圖層觸發的動做,能夠表示同步和異步(好比網絡請求),它最終會被轉換成Mutation再被傳遞到Reducer中;Mutation
:只能表示同步操做,至關於Redux模式中的Action,最終被傳入Reducer中參與新狀態的計算;mutate():
mutate()
是Reactor中的一個方法,用來將用戶觸發的Action
轉換成Mutation
,mutate()
的存在使得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強大的描述能力,咱們能夠用一致的方式來處理同步和異步代碼。
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
}
複製代碼
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
事件。
RxSwfit
很好的結合在了一塊兒,能提供較爲完善的函數響應式(FRP)開發體驗;Redux模式對於大部分應用來講仍是過於沉重了,並且Swift的語言特性也不像JavaScript那樣靈活,不少樣板代碼沒法避免。因此這裏總結了另外一套簡化的方案,但願能在享受單向數據流優點的同時減輕使用者的負擔。
詳細的代碼請看Demo中的Custom
文件夾:
實現很是簡單,核心是一個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
分紅三個部分:
// <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賦值給
ViewModel
的state
變量便可:self.state = localState
;
定義對外暴露的可觀察變量(Getter)
ViewModel須要暴露一些能讓視圖進行綁定的可觀察對象(Observable),Store
中提供了一個名爲rxState
的Observable<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
的協議,這裏主要參考了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
協議存在的意義在於將視圖層的事件綁定規範化,防止綁定操做的代碼散落在各處下降可讀性。
按照以上流程實現的頁面數據流以下:
ViewModel
中的邏輯;ViewModel
中執行具體的業務邏輯,並經過performStateUpdate
修改保存在State中的狀態變量;這樣能保證一個頁面的數據始終按照預期的方式來變化,並且單向數據流的特色使得咱們能夠像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:
若是你爲全部StateType
類型實現序列化和反序列化的操做,甚至能夠實現相似redux-devtools這樣的Time Travel
功能,這裏就再也不繼續引伸了。
引入Rx模式須要多方面的考慮,本文僅針對狀態管理這一點做了介紹,上面介紹的三種方案各有特色,最終的選擇仍是要結合項目的實際狀況來判斷。