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

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

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

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

一個復(fù)雜的頁面通常需要維護(hù)大量的變量來表示其運(yùn)行期間的各種狀態(tài),在MVVM中頁面大部分的狀態(tài)和邏輯都通過ViewModel來維護(hù),在常見的寫法中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),這樣代碼反而會變得更加難以維護(hù)。

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

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

Redux - ReSwift

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

雙向綁定和單向綁定

要理解Redux首先要明白Redux是為了解決什么問題而生的,Redux為應(yīng)用提供統(tǒng)一的狀態(tài)管理,并實(shí)現(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ù)進(jìn)行操作然后刷新UI。視圖層即會響應(yīng)數(shù)據(jù)改變的事件,又會直接訪問和修改數(shù)據(jù),這就是一個雙向綁定的關(guān)系:

雙向綁定

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

實(shí)現(xiàn)

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

redux

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

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

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

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

    /// 設(shè)置數(shù)據(jù)的Action
    struct ActionSetMessage: Action {
        var news: [MsgItem] = []
    }
    
    /// 移除某項(xiàng)數(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í)是借鑒了函數(shù)式的一些思想,首先Redux強(qiáng)調(diào)了數(shù)據(jù)的不可變性(Immutable),簡單來說就是一個數(shù)據(jù)模型在創(chuàng)建之后就不可被修改,那當(dāng)我們要修改Model某個屬性時要怎么辦呢?答案就是創(chuàng)建一個新的Model,Reducer的作用就體現(xiàn)在這里:

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

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

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

    // Store.swift中的實(shí)現(xiàn)
    open func _defaultDispatch(action: Action) {
        guard !isDispatching else {
            raiseFatalError("...")
        }
        isDispatching = true
        let newState = reducer(action, state) // 1. 通過reducer計(jì)算出新的state
        isDispatching = false
    
        state = newState // 2. 直接將新的state賦值到當(dāng)前的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ù)進(jìn)行相應(yīng)的修改
        switch action {
        case let setMessage as ActionSetMessage: // 設(shè)置列表數(shù)據(jù)
            state.newsList = setMessage.news
        case let remove as ActionRemoveMessage: // 移除某一項(xiàng)
            state.newsList.remove(at: remove.index)
        default:
            break
        }
        // 最后直接返回修改后的整個State結(jié)構(gòu)體
        return state
    }
    

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

分析

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

  • 優(yōu)點(diǎn)

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

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

  • 缺點(diǎn)

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

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

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

      樣板代碼

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

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

Vuex - ReactorKit

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

實(shí)現(xiàn)

ReSwift不同的是ReactorKit的實(shí)現(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)建一個實(shí)現(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將這個概念更加細(xì)化,拆分成了兩個部分:ActionMutation

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

    mutate()是Reactor中的一個方法,用來將用戶觸發(fā)的Action轉(zhuǎn)換成Mutation,mutate()的存在使得Action可以表示異步操作,因?yàn)闊o論是異步還是同步的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>類型的實(shí)例,得益于Rx強(qiáng)大的描述能力,我們可以用一致的方式來處理同步和異步代碼。

  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指的是實(shí)現(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實(shí)際上是一個中間層,它負(fù)責(zé)管理視圖的狀態(tài),并作為視圖和具體業(yè)務(wù)邏輯之間通訊的橋梁。

此外ReactorKit希望我們的所有代碼都通過函數(shù)響應(yīng)式(FRP)的風(fēng)格來編寫,這從它的API設(shè)計(jì)上可以看出: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)一進(jìn)行事件綁定的地方
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方法是視圖層進(jìn)行事件綁定的地方,我們將VC的viewDidLoad作為一個事件源,將其轉(zhuǎn)換成網(wǎng)絡(luò)請求的Action之后綁定到reactor.action上,這樣當(dāng)VC的viewDidLoad被調(diào)用時該事件源就會發(fā)出一個事件并觸發(fā)Reactor中網(wǎng)絡(luò)請求的操作。

這樣的寫法是更加FRP,一切都是事件流,但是實(shí)際用起來并不是那么完美。首先我們需要為用到的所有UI組件提供Rx擴(kuò)展(上面的例子使用了RxViewController這個庫);其次這對reactor實(shí)例初始化的時機(jī)有更加嚴(yán)格的要求,因?yàn)?code>bind方法是在reactor實(shí)例初始化的時候自動調(diào)用的,所以不能在viewDidLoad中初始化,否則會錯過viewDidLoad事件。

分析

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

另一種簡化方案

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

詳細(xì)的代碼請看Demo中的Custom文件夾:

Demo

實(shí)現(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)的可觀察對象,當(dāng)狀態(tài)發(fā)生改變時`rxState`會發(fā)送相應(yīng)的事件
    public var rxState: Observable<State> {
        return _state.asObservable()
    }
    
    /// 強(qiáng)制更新狀態(tài),所有的觀察者都會收到next事件
    public func forceUpdateState() {
        _state.onNext(state)
    }
    
    /// 在一個閉包中更新狀態(tài)變量,當(dāng)閉包返回后一次性應(yīng)用所有的更新,用于更新狀態(tài)變量
    public func performStateUpdate(_ updater: (inout State) -> Void) {
        updater(&self.state)
        forceUpdateState()
    }
    ...
}

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

ViewModel

在實(shí)際開發(fā)中我讓ViewModel來處理狀態(tài)管理和變更的邏輯,再來實(shí)現(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)變量都需要定義在一個單獨(dú)的實(shí)現(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來訪問當(dāng)前的狀態(tài)變量。而修改狀態(tài)變量則通過一個performStateUpdate方法來完成,方法簽名如下:

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

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

    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)點(diǎn):

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

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

    這部分邏輯定義在ViewModel的Rx擴(kuò)展中,對外提供可觀察的屬性,這里定義了視圖層需要綁定的所有狀態(tài)。這部分的作用相當(dāng)于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(...)
    

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

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

    ViewModel還需要接收視圖層的事件以觸發(fā)具體的業(yè)務(wù)邏輯,如果這一步通過Rx綁定的方式來完成的話,會對業(yè)務(wù)層代碼的編寫方式帶來很多限制(參考上面的ReactorKit)。所以這部分不做過多的封裝,還是通過方法的形式來對外暴露接口,這部分就相當(dāng)于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

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

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

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

  • 實(shí)現(xiàn)一個doBinding方法,所有的Rx事件綁定都放在這個方法中完成:

    func doBinding(_ vm: MessageViewModel) {
        vm.rx.sections
            .drive(self.tableView.rx.items(dataSource: dataSource))
            .disposed(by: self.disposeBag)   
    }
    
  • 在合適的時機(jī)初始化viewModel屬性:

    override func viewDidLoad() {
        super.viewDidLoad()
        // 初始化ViewModel
        self.viewModel = MessageViewModel(state: MessageState())
    }
    

    當(dāng)viewModel初始化完成后會自動調(diào)用doBinding方法進(jìn)行綁定,并且在實(shí)例的生命周期中只會被執(zhí)行一次。

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

數(shù)據(jù)流

按照以上流程實(shí)現(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ù)流的特點(diǎn)使得我們可以像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()
}

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

States Change Log

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

總結(jié)

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 學(xué)習(xí)必備要點(diǎn): 首先弄明白,Redux在使用React開發(fā)應(yīng)用時,起到什么作用——狀態(tài)集中管理 弄清楚Redux是...
    賀賀v5閱讀 9,079評論 10 58
  • Angular2和Rx的相關(guān)知識可以看我的Angular 2.0 從0到1系列 第一節(jié):初識Angular-CLI...
    接灰的電子產(chǎn)品閱讀 22,810評論 39 59
  • http://gaearon.github.io/redux/index.html ,文檔在 http://rac...
    jacobbubu閱讀 80,434評論 35 198
  • 古老的土城墻 訴說著歷史的滄桑 也見證著今天的輝煌 這里曾經(jīng) 也許金戈鐵馬 有些戰(zhàn)爭的悲涼 也有著 日出而作 日落...
    趙國杰閱讀 117評論 2 6
  • 精讀營招募的時候我是很猶豫的,因?yàn)榈谌煳矣袀€考試,我把照顧孩子剩下的精力全部用來準(zhǔn)備考試了,但我不想錯過...
    Cat_e1cc閱讀 366評論 1 4

友情鏈接更多精彩內(nèi)容