談談RxSwift和狀態管理

L-Zephyr發表於2018-08-10

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

本文篇幅較長,主要圍繞著狀態管理這一話題進行介紹,前兩個部分介紹了前端領域中React和Vue所採用的狀態管理模式及其在Swift中的實現,最後介紹了另一種簡化的狀態管理方案。不會涉及複雜的Rx特性,閱讀前對Rx有一些基本的瞭解即可。

為什麼狀態管理這麼重要

一個複雜的頁面通常需要維護大量的變數來表示其執行期間的各種狀態,在MVVM中頁面大部分的狀態和邏輯都通過ViewModel來維護,在常見的寫法中ViewModel和檢視之間通常用Delegate來通訊,比如說在資料改變的時候通知檢視層更新UI等等:

MVVM

在這種模式中,ViewModel的狀態更新之後需要我們呼叫Delegate手動通知檢視層。而在Rx中這一層關係被淡化了,由於Rx是響應式的,設定好繫結關係後ViewModel只需要改變資料的值,Rx會自動的通知每一個觀察者:

Rx

Rx為我們隱藏了通知檢視的過程,首先這樣的好處是明顯的:ViewModel可以更加專注於資料本身,不用再去管UI層的邏輯;但是濫用這個特性也會帶來麻煩,大量的可觀察變數和繫結操作會讓邏輯變得含糊不清,修改一個變數的時候可能會導致一系列難以預料的連鎖反應,這樣程式碼反而會變得更加難以維護。

想要更好的過渡到響應式程式設計,一個統一的狀態管理方案是不可或缺的。在這一塊前端領域有不少成熟的實踐方案,Swift中也有一些開源庫對其進行了實現,其中的思想我們可以先來參考一下。

下面的介紹中所涉及的示例程式碼在:github.com/L-Zephyr/My…

Redux - ReSwift

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中所描述的資料流:

redux

其中的State就是應用的狀態,也就是我們的Model部分,先不管這裡的ActionReducer等概念,從圖中可以看到State和View是有著直接的繫結關係的,而View的事件則會通過ActionStore等一系列操作間接的改變State,下面來詳細的介紹一下Redux的資料流的實現以及所涉及到的概念:

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

  2. 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所攜帶的資料儲存在其成員變數中。

  3. 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))
    複製程式碼
  4. 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模式並不太適用。

Vuex - ReactorKit

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

實現

ReSwift不同的是ReactorKit的實現本身便於基於RxSwift,所以不必再考慮如何與Rx結合,下面是ReactorKit中資料的流程圖:

Reactor

大體流程與Redux類似,不同的是Store變成了Reactor,這是ReactorKit引入的一個新概念,它不要求在全域性範圍統一管理狀態,而是每個元件管理各自的狀態,所以每個檢視元件都有各自所對應的Reactor

具體的程式碼請看Demo中的ReactorKit資料夾,各個部分的含義如下:

  1. 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需要定義StateActionMutation這三個部分,後面會一一介紹。

    首先比起Redux這裡多了一個Mutation的概念,在Redux中由於Action直接與Reducer中的操作對應,所以Action只能用來表示同步的操作。ReactorKit將這個概念更加細化,拆分成了兩個部分:ActionMutation

    • Action:檢視層觸發的動作,可以表示同步和非同步(比如網路請求),它最終會被轉換成Mutation再被傳遞到Reducer中;
    • Mutation:只能表示同步操作,相當於Redux模式中的Action,最終被傳入Reducer中參與新狀態的計算;
  2. mutate():

    mutate()是Reactor中的一個方法,用來將使用者觸發的Action轉換成Mutationmutate()的存在使得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強大的描述能力,我們可以用一致的方式來處理同步和非同步程式碼。

  3. 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
    }
    複製程式碼
  4. 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事件。

分析

  • 優點
    • 相比ReSwift簡化了一些流程,並且以元件為單位來管理各自的狀態,相比起來更容易在現有工程中引入;
    • RxSwfit很好的結合在了一起,能提供較為完善的函式響應式(FRP)開發體驗;
  • 缺點
    • 因為核心思想還是Redux模式,所以模板程式碼過多的問題還是無法避免;

另一種簡化方案

Redux模式對於大部分應用來說還是過於沉重了,而且Swift的語言特性也不像JavaScript那樣靈活,很多樣板程式碼無法避免。所以這裡總結了另一套簡化的方案,希望能在享受單向資料流優勢的同時減輕使用者的負擔。

詳細的程式碼請看Demo中的Custom資料夾:

Demo

實現非常簡單,核心是一個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來處理狀態管理和變更的邏輯,再來實現一次上面的那個例子,將一個業務方的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賦值給ViewModelstate變數即可:self.state = localState
    • *便於測試:*單元測試時可以通過檢查State型別的變數來進行測試;
  • 定義對外暴露的可觀察變數(Getter)

    ViewModel需要暴露一些能讓檢視進行繫結的可觀察物件(Observable),Store中提供了一個名為rxStateObservable<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

檢視層需要實現一個名為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協議存在的意義在於將檢視層的事件繫結規範化,防止繫結操作的程式碼散落在各處降低可讀性。

資料流

按照以上流程實現的頁面資料流如下:

資料流

  1. 檢視(View)中的事件觸發時,直接呼叫相應的方法觸發ViewModel中的邏輯;
  2. ViewModel中執行具體的業務邏輯,並通過performStateUpdate修改儲存在State中的狀態變數;
  3. 狀態變數發生改變之後,通過Rx的繫結自動通知檢視層更新UI;

這樣能保證一個頁面的資料始終按照預期的方式來變化,而且單向資料流的特點使得我們可以像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:

States Change Log

如果你為所有StateType型別實現序列化和反序列化的操作,甚至可以實現類似redux-devtools這樣的Time Travel功能,這裡就不再繼續引申了。

總結

引入Rx模式需要多方面的考慮,本文僅針對狀態管理這一點作了介紹,上面介紹的三種方案各有特點,最終的選擇還是要結合專案的實際情況來判斷。

相關文章