談?wù)凴xSwift和狀態(tài)管理

前段時間在RxSwift上做了一些實踐彻亲,Rx確實是一個強大的工具,但同時也是一把雙刃劍室叉,如果濫用的話反而會帶來副作用睹栖,本文就引入Rx模式之后如何更好的管理應(yīng)用的狀態(tài)和邏輯做了一些粗淺的總結(jié)硫惕。

本文篇幅較長茧痕,主要圍繞著狀態(tài)管理這一話題進行介紹,前兩個部分介紹了前端領(lǐng)域中React和Vue所采用的狀態(tài)管理模式及其在Swift中的實現(xiàn)恼除,最后介紹了另一種簡化的狀態(tài)管理方案踪旷。不會涉及復(fù)雜的Rx特性,閱讀前對Rx有一些基本的了解即可豁辉。

為什么狀態(tài)管理這么重要

一個復(fù)雜的頁面通常需要維護大量的變量來表示其運行期間的各種狀態(tài)令野,在MVVM中頁面大部分的狀態(tài)和邏輯都通過ViewModel來維護,在常見的寫法中ViewModel和視圖之間通常用Delegate來通訊徽级,比如說在數(shù)據(jù)改變的時候通知視圖層更新UI等等:

MVVM

在這種模式中气破,ViewModel的狀態(tài)更新之后需要我們調(diào)用Delegate手動通知視圖層。而在Rx中這一層關(guān)系被淡化了餐抢,由于Rx是響應(yīng)式的现使,設(shè)定好綁定關(guān)系后ViewModel只需要改變數(shù)據(jù)的值低匙,Rx會自動的通知每一個觀察者:

Rx

Rx為我們隱藏了通知視圖的過程,首先這樣的好處是明顯的:ViewModel可以更加專注于數(shù)據(jù)本身碳锈,不用再去管UI層的邏輯顽冶;但是濫用這個特性也會帶來麻煩,大量的可觀察變量和綁定操作會讓邏輯變得含糊不清售碳,修改一個變量的時候可能會導(dǎo)致一系列難以預(yù)料的連鎖反應(yīng)强重,這樣代碼反而會變得更加難以維護。

想要更好的過渡到響應(yīng)式編程贸人,一個統(tǒng)一的狀態(tài)管理方案是不可或缺的间景。在這一塊前端領(lǐng)域有不少成熟的實踐方案,Swift中也有一些開源庫對其進行了實現(xiàn)艺智,其中的思想我們可以先來參考一下拱燃。

下面的介紹中所涉及的示例代碼在:https://github.com/L-Zephyr/MyDemos/tree/master/RxStateDemo

Redux - ReSwift

Redux是Facebook所提出的基于Flux改良的一種狀態(tài)管理模式力惯,在Swift中有一個名為ReSwift的開源項目實現(xiàn)了這個模式碗誉。

雙向綁定和單向綁定

要理解Redux首先要明白Redux是為了解決什么問題而生的,Redux為應(yīng)用提供統(tǒng)一的狀態(tài)管理父晶,并實現(xiàn)了單向的數(shù)據(jù)流哮缺。所謂的單向綁定雙向綁定所描述的都是視圖(View)和數(shù)據(jù)(Model)之間的關(guān)系:

比方說有一個展示消息的頁面,首先需要從網(wǎng)絡(luò)加載最新的消息甲喝,在MVC中我們可以這樣寫:

class NormalMessageViewController: UIViewController {
    var msgList: [MsgItem] = [] // 數(shù)據(jù)源
    
    // 網(wǎng)絡(luò)請求
    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. 請求結(jié)束后更新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方法中我們通過網(wǎng)絡(luò)請求修改了數(shù)據(jù)msgList尝苇,一旦msgList發(fā)生改變必須刷新UI,顯然視圖的狀態(tài)跟數(shù)據(jù)是同步的埠胖;在tableView上刪除消息時糠溜,視圖層直接對數(shù)據(jù)進行操作然后刷新UI。視圖層即會響應(yīng)數(shù)據(jù)改變的事件直撤,又會直接訪問和修改數(shù)據(jù)非竿,這就是一個雙向綁定的關(guān)系:

雙向綁定

雖然在這個例子中看起來非常簡單,但是當頁面比較復(fù)雜的時候UI操作和數(shù)據(jù)操作混雜在一起會讓邏輯變得混亂谋竖『熘看到這里單向綁定的含義就很明顯了,它去掉了View -> Model的這一層關(guān)系蓖乘,視圖層不能直接對數(shù)據(jù)進行修改锤悄,它只能通過某種機制向數(shù)據(jù)層傳遞事件,并在數(shù)據(jù)改變的時候刷新UI嘉抒。

實現(xiàn)

為了構(gòu)造單向數(shù)據(jù)流零聚,Redux引入了一系列概念,這是Redux中所描述的數(shù)據(jù)流:

redux

其中的State就是應(yīng)用的狀態(tài),也就是我們的Model部分隶症,先不管這里的Action容诬、Reducer等概念,從圖中可以看到State和View是有著直接的綁定關(guān)系的沿腰,而View的事件則會通過Action览徒、Store等一系列操作間接的改變State,下面來詳細的介紹一下Redux的數(shù)據(jù)流的實現(xiàn)以及所涉及到的概念:

  1. View
    顧名思義颂龙,View就是視圖习蓬,用戶在視圖上的操作事件不會直接修改模型,而是會被映射成一個個Action措嵌。

  2. Action
    Action表示一個對數(shù)據(jù)操作的請求躲叼,Action會被發(fā)送到Store中,這是對模型數(shù)據(jù)進行修改的唯一辦法企巢。

    在ReSwift中有一個名為Action的協(xié)議(僅作標記用的空協(xié)議)枫慷,對于Model中數(shù)據(jù)的每個操作,比如說設(shè)置一個值浪规,都需要有一個對應(yīng)的Action:

    /// 設(shè)置數(shù)據(jù)的Action
    struct ActionSetMessage: Action {
        var news: [MsgItem] = []
    }
    
    /// 移除某項數(shù)據(jù)的Action
    struct ActionRemoveMessage: Action {
        var index: Int
    }
    

    struct類型來表示一個Action或听,Action所攜帶的數(shù)據(jù)保存在其成員變量中。

  3. Store和State
    就像上面所提到的笋婿,State表示了應(yīng)用中的Model數(shù)據(jù)誉裆,而Store則是存放State的地方;在Redux中Store是一個全局的容器缸濒,所有組件的狀態(tài)都被保存在里面足丢;Store接受一個Action,然后修改數(shù)據(jù)并通知視圖層更新UI庇配。

    如下所示斩跌,每一個頁面和組件都有各自的狀態(tài)以及用來儲存狀態(tài)的Store:

    // State
    struct ReduxMessageState: StateType {
        var newsList: [MsgItem] = []
    }
    
    // Store,直接使用ReSwift的Store類型來初始化即可捞慌,初始化時要指定reducer和狀態(tài)的初始值
    let newsStore = Store<ReduxMessageState>(reducer: reduxMessageReducer, state: nil)
    

    Store通過一個dispatch方法來接收Action耀鸦,視圖調(diào)用這個方法來向Store傳遞Action:

    messageStore.dispatch(ActionRemoveMessage(index: 0))
    
  4. Reducer
    Reducer是一個比較特殊的函數(shù),這里其實是借鑒了函數(shù)式的一些思想卿闹,首先Redux強調(diào)了數(shù)據(jù)的不可變性(Immutable)揭糕,簡單來說就是一個數(shù)據(jù)模型在創(chuàng)建之后就不可被修改萝快,那當我們要修改Model某個屬性時要怎么辦呢锻霎?答案就是創(chuàng)建一個新的Model,Reducer的作用就體現(xiàn)在這里:

    Reducer是一個函數(shù)揪漩,它的簽名如下:

    (_ action: Action, _ state: StateType?) -> StateType
    

    接受一個表示動作的action和一個表示當前狀態(tài)的state旋恼,然后計算并返回一個新的State,隨后這個新的State會被更新到Store中:

    // Store.swift中的實現(xiàn)
    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上
    }
    

    應(yīng)用中所有數(shù)據(jù)模型的更新操作最終都通過Reducer來完成奄容,為了保證這一套流程可以正常的完成冰更,Reducer必須是一個純函數(shù):它的輸出只取決于輸入的參數(shù)产徊,不依賴任何外部變量,同樣也不能包含任何異步的操作蜀细。

    在這個例子中的Reducer是這樣寫的:

    func reduxMessageReducer(action: Action, state: ReduxMessageState?) -> ReduxMessageState {
        var state = state ?? ReduxMessageState()
        // 根據(jù)不同的Action對數(shù)據(jù)進行相應(yīng)的修改
        switch action {
        case let setMessage as ActionSetMessage: // 設(shè)置列表數(shù)據(jù)
            state.newsList = setMessage.news
        case let remove as ActionRemoveMessage: // 移除某一項
            state.newsList.remove(at: remove.index)
        default:
            break
        }
        // 最后直接返回修改后的整個State結(jié)構(gòu)體
        return state
    }
    

最后在視圖中實現(xiàn)StoreSubscriber協(xié)議接收State改變的通知并更新UI即可舟铜。詳細的代碼請看Demo中的Redux文件夾。

分析

Redux將View -> Model這一層關(guān)系分解成了View -> Action -> Store -> Model奠衔,每一個模塊只負責(zé)一件事情谆刨,數(shù)據(jù)始終沿著這條鏈路單向傳遞。

  • 優(yōu)點

    • 在處理大量狀態(tài)的時候單向數(shù)據(jù)流更加容易維護归斤,所有事件都通過唯一的入口dispatch手動觸發(fā)痊夭,數(shù)據(jù)的每一個處理過程都是透明的,這樣就可以追蹤到每一次的狀態(tài)變更操作脏里。在前端中Redux的配套工具redux-devtools就提供了一個名為Time Travel的功能她我,能夠回溯應(yīng)用的任意歷史狀態(tài)。

    • 全局Store有利于在多個組件之間共享狀態(tài)迫横。

  • 缺點

    • 首先Redux為它的數(shù)據(jù)流指定了大量的規(guī)則番舆,無疑會帶來更高的學(xué)習(xí)成本。

    • 在Redux的核心模型中并沒有考慮異步(Reducer是純函數(shù))矾踱,所以如網(wǎng)絡(luò)請求這樣的異步任務(wù)還需要通過ActionCreator之類的機制間接處理合蔽,進一步提升了復(fù)雜度。

    • 另一個被廣為詬病的缺點是介返,Redux會引入大量樣板代碼拴事,在上面這個簡單的例子中我們需要為頁面創(chuàng)建Store、State圣蝎、Reducer刃宵、Action等不同的結(jié)構(gòu):

      樣板代碼

      即便是修改一個狀態(tài)變量這樣簡單的操作都需要經(jīng)過這一套流程,這無疑會大大增加代碼量徘公。

綜上所述牲证,Redux模式雖然有許多優(yōu)點,但它帶來的成本也無法忽視关面。如果你的頁面和交互極其復(fù)雜或是多個頁面之間有大量的共享狀態(tài)的話可以考慮Redux坦袍,但是對于大部分應(yīng)用來說,Redux模式并不太適用等太。

Vuex - ReactorKit

Vue也是近年來十分熱門的前端框架之一捂齐,Vuex則是其專門為Vue提出的狀態(tài)管理模式,在Redux之上進行了一些優(yōu)化缩抡;而ReactorKit是一個Swift的開源庫奠宜,它的一些設(shè)計理念與Vuex十分相似,所以這里我將它們放在一起來講。

實現(xiàn)

ReSwift不同的是ReactorKit的實現(xiàn)本身便于基于RxSwift压真,所以不必再考慮如何與Rx結(jié)合娩嚼,下面是ReactorKit中數(shù)據(jù)的流程圖:

ReactorKit

大體流程與Redux類似,不同的是Store變成了Reactor滴肿,這是ReactorKit引入的一個新概念岳悟,它不要求在全局范圍統(tǒng)一管理狀態(tài),而是每個組件管理各自的狀態(tài)泼差,所以每個視圖組件都有各自所對應(yīng)的Reactor竿音。

具體的代碼請看Demo中的ReactorKit文件夾,各個部分的含義如下:

  1. Reactor:

    現(xiàn)在用ReactorKit來重寫上面的那個例子拴驮,首先需要為這個頁面創(chuàng)建一個實現(xiàn)了Reactor協(xié)議的類型MessageReactor

    class MessageReactor: Reactor {
        // 與Redux中的Action作用相同春瞬,可以是異步
        enum Action {
            case request
            case removeItem(Int)
        }
        // 表示修改狀態(tài)的動作(同步)
        enum Mutation {
            case setMessageList([MsgItem])
            case removeItem(Int)
        }
        // 狀態(tài)
        struct State {
            var newsList: [MsgItem] = []
        }
        ...
    }
    

    一個Reactor需要定義StateAction套啤、Mutation這三個部分宽气,后面會一一介紹。

    首先比起Redux這里多了一個Mutation的概念潜沦,在Redux中由于Action直接與Reducer中的操作對應(yīng)萄涯,所以Action只能用來表示同步的操作。ReactorKit將這個概念更加細化唆鸡,拆分成了兩個部分:ActionMutation

    • Action:視圖層觸發(fā)的動作涝影,可以表示同步和異步(比如網(wǎng)絡(luò)請求),它最終會被轉(zhuǎn)換成Mutation再被傳遞到Reducer中争占;
    • Mutation:只能表示同步操作燃逻,相當于Redux模式中的Action,最終被傳入Reducer中參與新狀態(tài)的計算臂痕;
  2. mutate():

    mutate()是Reactor中的一個方法伯襟,用來將用戶觸發(fā)的Action轉(zhuǎn)換成Mutationmutate()的存在使得Action可以表示異步操作握童,因為無論是異步還是同步的Action最后都會被轉(zhuǎn)換成同步的Mutation:

    func mutate(action: MessageReactor.Action) -> Observable<MessageReactor.Mutation> {
        switch action {
        case .request:
            // 1. 異步:網(wǎng)絡(luò)請求結(jié)束后將得到的數(shù)據(jù)轉(zhuǎn)換成Mutation
            return service.request().map { Mutation.setMessageList($0) }
        case .removeItem(let index):
            // 2. 同步:直接用just包裝一個Mutation
            return .just(Mutation.removeItem(index))
        }
    }
    

    值得一提的是姆怪,這里的mutate()方法返回的是一個Observable<Mutation>類型的實例,得益于Rx強大的描述能力澡绩,我們可以用一致的方式來處理同步和異步代碼稽揭。

  3. reduce():

    reduce()方法這里就沒太多可說的了,它扮演的角色與Redux中的Reducer一樣肥卡,唯一不同的是這里接受的是一個Mutation類型溪掀,但本質(zhì)是一樣的:

    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
    }
    
  4. Service

    圖中還有一個與mutate()產(chǎn)生交互的Service對象,Service指的是實現(xiàn)具體業(yè)務(wù)邏輯的地方召调,Reactor會通過各個Service對象來執(zhí)行具體的業(yè)務(wù)邏輯膨桥,比如說網(wǎng)絡(luò)請求:

    protocol MessageServiceType {
        /// 網(wǎng)絡(luò)請求
        func request() -> Observable<[MsgItem]>
    }
    
    final class MessageService: MessageServiceType {
        func request() -> Observable<[MsgItem]> {
            return MessageProvider
             .rx
             .request(.news)
             .mapModel([MsgItem].self)
             .asObservable()
        }
    }
    

    看到這里Reactor的本質(zhì)基本上已經(jīng)明了:Reactor實際上是一個中間層蛮浑,它負責(zé)管理視圖的狀態(tài)唠叛,并作為視圖和具體業(yè)務(wù)邏輯之間通訊的橋梁只嚣。

此外ReactorKit希望我們的所有代碼都通過函數(shù)響應(yīng)式(FRP)的風(fēng)格來編寫,這從它的API設(shè)計上可以看出:Reactor類型中沒有提供如dispatch這樣的方法艺沼,而是只提供了一個Subject類型的變量action

var action: ActionSubject<Action> { get }

在Rx中Subject既是觀察者又是可觀察對象册舞,常常扮演一個中間橋梁的角色。視圖上所有的Action都通過Rx綁定到action變量上障般,而不是通過手動觸發(fā)的方式:比方說我們想在viewDidLoad的時候發(fā)起一個網(wǎng)絡(luò)請求调鲸,常規(guī)的寫法是這樣的:

override func viewDidLoad() {
    super.viewDidLoad()
    service.request() // 手動觸發(fā)一個網(wǎng)絡(luò)請求動作
}

ReactorKit所推崇的函數(shù)式風(fēng)格是這樣的:

// bind是統(tǒng)一進行事件綁定的地方
func bind(reactor: MessageReactor) {
    self.rx.viewDidLoad // 1. 將viewDidLoad作為一個可觀察的事件
        .map { Reactor.Action.request } // 2. 將viewDidLoad事件轉(zhuǎn)成Action
        .bind(to: reactor.action) // 3. 綁定到action變量上
        .disposed(by: self.disposeBag)
    // ...
}

bind方法是視圖層進行事件綁定的地方,我們將VC的viewDidLoad作為一個事件源挽荡,將其轉(zhuǎn)換成網(wǎng)絡(luò)請求的Action之后綁定到reactor.action上藐石,這樣當VC的viewDidLoad被調(diào)用時該事件源就會發(fā)出一個事件并觸發(fā)Reactor中網(wǎng)絡(luò)請求的操作。

這樣的寫法是更加FRP定拟,一切都是事件流于微,但是實際用起來并不是那么完美。首先我們需要為用到的所有UI組件提供Rx擴展(上面的例子使用了RxViewController這個庫)青自;其次這對reactor實例初始化的時機有更加嚴格的要求株依,因為bind方法是在reactor實例初始化的時候自動調(diào)用的,所以不能在viewDidLoad中初始化延窜,否則會錯過viewDidLoad事件恋腕。

分析

  • 優(yōu)點
    • 相比ReSwift簡化了一些流程,并且以組件為單位來管理各自的狀態(tài)逆瑞,相比起來更容易在現(xiàn)有工程中引入荠藤;
    • RxSwfit很好的結(jié)合在了一起,能提供較為完善的函數(shù)響應(yīng)式(FRP)開發(fā)體驗获高;
  • 缺點
    • 因為核心思想還是Redux模式商源,所以模板代碼過多的問題還是無法避免;

另一種簡化方案

Redux模式對于大部分應(yīng)用來說還是過于沉重了谋减,而且Swift的語言特性也不像JavaScript那樣靈活牡彻,很多樣板代碼無法避免。所以這里總結(jié)了另一套簡化的方案出爹,希望能在享受單向數(shù)據(jù)流優(yōu)勢的同時減輕使用者的負擔庄吼。

詳細的代碼請看Demo中的Custom文件夾:

Demo

實現(xiàn)非常簡單,核心是一個Store類型:

public protocol StateType { }

public class Store<ConcreteState>: StoreType where ConcreteState: StateType {
    public typealias State = ConcreteState

    /// 狀態(tài)變量严就,一個只讀類型的變量
    public private(set) var state: State
    
    /// 狀態(tài)變量對應(yīng)的可觀察對象总寻,當狀態(tài)發(fā)生改變時`rxState`會發(fā)送相應(yīng)的事件
    public var rxState: Observable<State> {
        return _state.asObservable()
    }
    
    /// 強制更新狀態(tài),所有的觀察者都會收到next事件
    public func forceUpdateState() {
        _state.onNext(state)
    }
    
    /// 在一個閉包中更新狀態(tài)變量梢为,當閉包返回后一次性應(yīng)用所有的更新渐行,用于更新狀態(tài)變量
    public func performStateUpdate(_ updater: (inout State) -> Void) {
        updater(&self.state)
        forceUpdateState()
    }
    ...
}

其中StateType是一個空協(xié)議嚼摩,僅作為類型約束用市殷;Store作為一個基類,負責(zé)保存組件的狀態(tài),以及管理狀態(tài)更新的數(shù)據(jù)源柜候,核心代碼非常簡單柿祈,下面來看一下實際應(yīng)用仗岖。

ViewModel

在實際開發(fā)中我讓ViewModel來處理狀態(tài)管理和變更的邏輯慰丛,再來實現(xiàn)一次上面的那個例子,將一個業(yè)務(wù)方的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)
    }
    ...
}

各個部分的含義如下:

  • 定義頁面的狀態(tài)變量

    描述一個頁面所需的所有狀態(tài)變量都需要定義在一個單獨的實現(xiàn)了StateType協(xié)議的struct中:

    struct MessageState: StateType {
        var msgList: [MsgItem] = [] // 原始數(shù)據(jù)
    }
    

    從前面的代碼中可以看到Store中有一個只讀的state屬性:

    public private(set) var state: State
    

    業(yè)務(wù)方的ViewModel直接通過self.state來訪問當前的狀態(tài)變量套鹅。而修改狀態(tài)變量則通過一個performStateUpdate方法來完成站蝠,方法簽名如下:

    public func performStateUpdate(_ updater: (inout State) -> Void)
    

    ViewModel在修改狀態(tài)變量的時候通過updater閉包中的參數(shù)直接進行修改:

    performStateUpdate { $0.msgList = [...] } // 修改狀態(tài)變量
    

    執(zhí)行完畢后頁面的狀態(tài)會被更新,所綁定的UI組件也會接受到狀態(tài)更新的事件卓鹿。這樣一來能避免為每一個狀態(tài)變量創(chuàng)建一個Action菱魔,簡化了流程,同時所有更新狀態(tài)的操作都由經(jīng)過同一個入口吟孙,有利于之后的分析澜倦。

    統(tǒng)一管理狀態(tài)變量有以下幾個優(yōu)點:

    • 邏輯清晰:在瀏覽頁面的代碼時只要查看這個類型就能知道哪些變量是需要特別關(guān)注的;
    • 頁面持久化:只需序列化這個結(jié)構(gòu)體就能夠保存這個頁面的全部信息拔疚,在恢復(fù)時只需要將反序列化出來的State賦值給ViewModelstate變量即可:self.state = localState肥隆;
    • 便于測試:單元測試時可以通過檢查State類型的變量來進行測試;
  • 定義對外暴露的可觀察變量(Getter)

    ViewModel需要暴露一些能讓視圖進行綁定的可觀察對象(Observable)稚失,Store中提供了一個名為rxStateObservable<State>類型對象作為狀態(tài)更新的統(tǒng)一事件源栋艳,但是為了更加便于視圖層使用,我們需要將其進一步細化句各。

    這部分邏輯定義在ViewModel的Rx擴展中吸占,對外提供可觀察的屬性,這里定義了視圖層需要綁定的所有狀態(tài)凿宾。這部分的作用相當于Getter矾屯,是視圖層從ViewModel中獲取數(shù)據(jù)源的接口:

    extension Reactive where Base: MessageViewModel {
        var sections: Observable<[MessageTableSectionModel]> {
            return base
              .rxState // 從統(tǒng)一的事件源rxState中分流
                .map({ (state) -> [MessageTableSectionModel] in
                    // 將VM中的后端原始模型類型轉(zhuǎn)換成UI層可以直接使用的視圖模型
                    return [
                        MessageTableSectionModel(items: state.msgList.map { MessageTableCellModel.news($0) })
                    ]
                })
        }   
    }
    

    這樣一來視圖層不需要關(guān)心State中的數(shù)據(jù)類型,直接通過rx屬性來獲取自己需要觀察的屬性即可:

    // 視圖層直接觀察sections初厚,不需要關(guān)心內(nèi)部的轉(zhuǎn)換邏輯
    vm.rx.sections.subscribe(...)
    

    為什么要將視圖層使用的接口定義在擴展中件蚕,而不是直接觀察基類中的rxState

    • 定義在Rx擴展中的變量可以直接通過ViewModel的rx屬性訪問到,便于視圖層使用产禾;
    • State中的原始數(shù)據(jù)可能需要一定轉(zhuǎn)換才能讓視圖層使用(比如上面將原始的MsgItem類型轉(zhuǎn)換成TableView可以直接使用的SectionModel模型)排作,這部分的邏輯適合放在擴展的計算屬性中,讓視圖層更加純粹亚情;
  • 對外提供的方法(Action)

    ViewModel還需要接收視圖層的事件以觸發(fā)具體的業(yè)務(wù)邏輯妄痪,如果這一步通過Rx綁定的方式來完成的話,會對業(yè)務(wù)層代碼的編寫方式帶來很多限制(參考上面的ReactorKit)楞件。所以這部分不做過多的封裝衫生,還是通過方法的形式來對外暴露接口裳瘪,這部分就相當于Action,不過這樣的代價是Action無法再通過統(tǒng)一的接口來派發(fā):

    class MessageViewModel: Store<MessageState> { 
        // 請求
        func request() {
            state.loadingState = .loading
            MessageProvider.rx
                .request(.news)
                .map([MsgItem].self)
                .subscribe(onSuccess: { (items) in
                    // 請求完成后改變state中響應(yīng)的變量罪针,UI層會自動響應(yīng)
                    self.performStateUpdate {
                        $0.msgList = items
                        $0.loadingState = .normal
                    }
                }, onError: { error in
                    self.performStateUpdate { $0.loadingState = .normal }
                })
                .disposed(by: self.disposeBag)
        }
    }
    

    我們之前已經(jīng)將狀態(tài)和UI完全分離開來了彭羹,所以在ViewModel的邏輯中只需要關(guān)心state中的狀態(tài)即可,不需要關(guān)心與視圖層的交互站故,所以以這種方式編寫的代碼同樣也是十分清晰的皆怕。

View

視圖層需要實現(xiàn)一個名為View的協(xié)議毅舆,這里主要參考了ReactorKit中的設(shè)計:

/// 視圖層協(xié)議
public protocol View: class {
    /// 用于聲明該視圖對應(yīng)的ViewModel的類型
    associatedtype ViewModel: StoreType
    
    /// ViewModel的實例西篓,有默認實現(xiàn),視圖層需要在合適的時機初始化
    var viewModel: ViewModel? { set get }
    
    /// 視圖層實現(xiàn)這個方法憋活,并在其中進行綁定
    func doBinding(_ vm: ViewModel)
}

對于視圖層來說岂津,它需要做兩件事:

  • 實現(xiàn)一個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初始化完成后會自動調(diào)用doBinding方法進行綁定悦即,并且在實例的生命周期中只會被執(zhí)行一次吮成。

在視圖層中對于各種狀態(tài)的綁定是很重要的一個環(huán)節(jié),View協(xié)議存在的意義在于將視圖層的事件綁定規(guī)范化辜梳,防止綁定操作的代碼散落在各處降低可讀性粱甫。

數(shù)據(jù)流

按照以上流程實現(xiàn)的頁面數(shù)據(jù)流如下:

數(shù)據(jù)流
  1. 視圖(View)中的事件觸發(fā)時,直接調(diào)用相應(yīng)的方法觸發(fā)ViewModel中的邏輯作瞄;
  2. ViewModel中執(zhí)行具體的業(yè)務(wù)邏輯茶宵,并通過performStateUpdate修改保存在State中的狀態(tài)變量;
  3. 狀態(tài)變量發(fā)生改變之后宗挥,通過Rx的綁定自動通知視圖層更新UI乌庶;

這樣能保證一個頁面的數(shù)據(jù)始終按照預(yù)期的方式來變化,而且單向數(shù)據(jù)流的特點使得我們可以像Redux這樣追蹤所有狀態(tài)的變更契耿,比如說我們可以簡單的利用Swift的反射(Mirror)來將所有狀態(tài)的變更打印到控制臺中:

public func performStateUpdate(_ updater: (inout State) -> Void) {
    updater(&self.state)

    #if DEBUG
    StateChangeRecorder.shared.record(state, on: self) // 記錄狀態(tài)的變更
    #endif

    forceUpdateState()
}

實現(xiàn)的代碼在StateChangeRecorder.swift文件中瞒大,非常簡單只有不到100行。每當有狀態(tài)發(fā)生改變的時候就會在控制臺中打印一條Log:

States Change Log

如果你為所有StateType類型實現(xiàn)序列化和反序列化的操作搪桂,甚至可以實現(xiàn)類似redux-devtools這樣的Time Travel功能透敌,這里就不再繼續(xù)引申了。

總結(jié)

引入Rx模式需要多方面的考慮踢械,本文僅針對狀態(tài)管理這一點作了介紹酗电,上面介紹的三種方案各有特點,最終的選擇還是要結(jié)合項目的實際情況來判斷裸燎。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末顾瞻,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子德绿,更是在濱河造成了極大的恐慌荷荤,老刑警劉巖退渗,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蕴纳,居然都是意外死亡会油,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門古毛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翻翩,“玉大人,你說我怎么就攤上這事稻薇∩┒常” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵塞椎,是天一觀的道長桨仿。 經(jīng)常有香客問我,道長案狠,這世上最難降的妖魔是什么服傍? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮骂铁,結(jié)果婚禮上吹零,老公的妹妹穿的比我還像新娘。我一直安慰自己拉庵,他們只是感情好灿椅,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著名段,像睡著了一般阱扬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上伸辟,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天麻惶,我揣著相機與錄音,去河邊找鬼信夫。 笑死窃蹋,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的静稻。 我是一名探鬼主播警没,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼振湾!你這毒婦竟也來了杀迹?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤押搪,失蹤者是張志新(化名)和其女友劉穎树酪,沒想到半個月后浅碾,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡续语,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年垂谢,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疮茄。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡滥朱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出力试,到底是詐尸還是另有隱情徙邻,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布懂版,位于F島的核電站鹃栽,受9級特大地震影響躏率,放射性物質(zhì)發(fā)生泄漏躯畴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一薇芝、第九天 我趴在偏房一處隱蔽的房頂上張望蓬抄。 院中可真熱鬧,春花似錦夯到、人聲如沸嚷缭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阅爽。三九已至,卻和暖如春荐开,著一層夾襖步出監(jiān)牢的瞬間付翁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工晃听, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留百侧,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓能扒,卻偏偏與公主長得像佣渴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子初斑,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • 學(xué)習(xí)必備要點: 首先弄明白辛润,Redux在使用React開發(fā)應(yīng)用時,起到什么作用——狀態(tài)集中管理 弄清楚Redux是...
    賀賀v5閱讀 8,896評論 10 58
  • 本文將開始詳細分析如何搭建一個React應(yīng)用架構(gòu)见秤。 一. 前言 現(xiàn)在已經(jīng)有很多腳手架工具砂竖,如create-reac...
    字節(jié)跳動技術(shù)團隊閱讀 4,325評論 1 23
  • Angular2和Rx的相關(guān)知識可以看我的Angular 2.0 從0到1系列 第一節(jié):初識Angular-CLI...
    接灰的電子產(chǎn)品閱讀 22,667評論 39 59
  • http://gaearon.github.io/redux/index.html 灵迫,文檔在 http://rac...
    jacobbubu閱讀 79,951評論 35 198
  • 古老的土城墻 訴說著歷史的滄桑 也見證著今天的輝煌 這里曾經(jīng) 也許金戈鐵馬 有些戰(zhàn)爭的悲涼 也有著 日出而作 日落...
    趙國杰閱讀 100評論 2 6