我的狀態管理方案(仿 redux )
眾所周知,Redux 作為一款很火的管理程序狀態的庫,火到甚至有自己的生態。知名的比如 redux-saga、redux-observable 等等。還包括很多不知名和一些以 redux- 開頭的各種小庫。如果你嘗試做自己的狀態管理,那麼 redux 總是那麼一道坎,是你不得不面對的對手,因為你要用充分的理由證明你比 redux 優秀,至少某些點如此。所以本文的目的也是如此,先分析一波 redux 的優缺點,然後提出我的改進方案 Ractor,用代碼證明它比 redux 優秀的地方。
redux 有哪些優點?
1、單 store
redux 之前的狀態管理方案基本都是 flux 架構。flux 架構太笨重,多 store,dispatcher。redux 提出了單 store ,並用高階組件自動訂閱 store,很快吸引了一批粉絲。
2、action
redux 提出 action 數據化之後,前端第一次能看到程序發生了什麼,debug 和 開發體驗都有了一定的提升。
3、devtool
這也是早起的 redux 的一大賣點,得力於 action,可以在控制台看到程序狀態。
4、純的 reducer
redux 為了做到可控性,犧牲了自由性。reducer 和 副作用做了分離,然後由社區提供方案處理,比如 thunk 和 redux-saga。
redux 有哪些缺點?
因為要提出改進方案,我重點介紹 redux 的缺點,並不是否認 redux 的偉大,希望理解。
1、redux state 是一種 persistence state
有過實戰經驗的都有過這種體會,有些狀態不適合放在 redux 裡面,比如你的 Dialog 組件的 isOpen 狀態。剛開始你打算這麼設計:
class Dialog extends React.Component {n state = {isOpen: false}n}n
事後發現需要在其他組件修改 Dialog 的狀態,你需要組件通訊,也就是狀態共享。然後又進入另一個極端,一股腦兒全存 redux :
{dialogName: { isOpen: true }, ...otherState}n
這樣有個問題是即使 Dialog 組件被卸載了,這個狀態還在。這個狀態在此時不具備描述意義,僅僅是被遺棄的過度狀態。
再有追求點的人已經發現 redux 的不對勁的地方了,開始尋找其他方案,在各種方案中你開始覺得 redux 用的很累,並發誓下個項目絕不用 redux。
然而項目還是要寫的,接著有人乾脆用 mobx 做這種組件狀態...先不說2種 style 有沒有問題,至少要做到 action 的一致性才行吧。mobx 管理的狀態就不用響應 action 了嗎,給這種臨時狀態多一點關愛吧。
在我看來,redux 適合存放領域數據,比如後端領域的 User,Topic 這些。還有前端領域的 路由,緩存等等。像上面說的 isOpen 等前端狀態是 volatile state,是不適合存在 redux 裡面的,最好跟組件一起載入和銷毀。如果你對 redux 有更好的理解歡迎通過評論告訴我...
2、redux 沒有錯誤處理
雖然 reducer 理論上是只能寫"純"的代碼,理論上是不會出錯的。但是不得不思考,如果出錯了怎麼辦。store 和 reducer 都沒有這種處理手段。
3、單 store
單 store 是他的優點的同時也是他的缺點:類型不安全。
const state = store.getState()n
上面代碼的 state 的類型就沒法自動推導出來,所以 react- redux 的 connect 的用法也不安全:
// state 這裡只能是 anyn@connect(state => ({user: state.user}))n
在我看來之所以單 store 能成功是因為,這裡的單指的是 eventStream。只要能保證 eventStream 是唯一的,那麼日誌,action 調試這些功能都能實現。
稍微做了點調研,現在使用redux的都是被迫的,要麼是一些遺留項目,要麼是架構能力不足,擔心未來會出狀況,於是使用已"成熟"的方案:redux。至於他們的下一個項目有的寫出自己的方案,有的換更自由的 mobx。
4、不能主動觸發 action
比如你有個緩存策略,如果是 redux 的話你需要在組件或者其他域里
dispatch(getByCache)n
redux 不能主動觸發這個 getByCache
新的方案 -- ractor
思考了這麼多之後,愛恨交加,糾結很久還是覺得不在 redux 的基礎上改進了,而是模仿 redux,但略做改進。新的方案應該有 redux 的優點:action,single event stream,time travel。還要補足上面提到的缺點。那麼看看我是怎麼做到的。
這裡有個簡單的在線例子 counter 和一個帶狀態注入的 todolist ,可以先看看有個大概的印象。
基本概念
在這之前,先介紹下基本概念。
1、system
正如上文提到的,我認為的 single 指的是 single eventStream,這裡的 system 和 redux 的 store 是對應的。事件在 system 裡面傳播,store 們抓取 system 裡面的事件進行處理。不同的是 system 已經在 ractor 庫里生成了,不用手動 createStore。system 有幾個重要的方法:
system.actorOf(actor: Store, name?: string)
往我們的事件系統里掛載 Store
system.stop(actor: Store)
從事件系統中卸載指定的 Store,搭配上面的 actorOf 方法,Ractor 才有了熱載入的功能。
2、dispatch
往 system 中廣播事件。等同於 system.dispatch。
3、Store
Store 是一個抽象類。定義自己的 store 需要繼承這個類並實現 createReceive 方法。
import { AbstractActor, Receive } from "js-actor"nnabstract class Store<S> extends AbstractActor {n public state = {} as Sn public abstract createReceive(): Receiven}n
ractor 怎麼補足上文提到的缺點的?
1、多 store
上面提到的 Dialog 的狀態管理問題,我設計了熱載入的 store 來解決這類問題,類似於熱載入的 reducer。這是多 store 的優勢,又繼承了單 eventStream 的有點。偽代碼如下:
ChangeDialogState:
export class ChangeDialogState {n constructor(public isOpen: boolean)n}n
DialogStore:
import { Store } from "ractor"nimport { ChangeDialogState } from "./ChangeDialogState"nnexport class DialogStore extends Store<{ isOpen: boolean }> {n public state = { isOpen: false }n public createReceive() {n return this.receiveBuilder()n .match(ChangeDialogState, changeDialogState => this.setState({isOpen: changeDialogState.isOpen}))n .build()n }n}n
Dialog.tsx:
import * as React from "react"nimport { connect } from "ractor-react"nimport { DialogStore } from "./DialogStore"nimport { dispatch } from "ractor"nimport { ChangeDialogState } from "./ChangeDialogState"nn@connect(CounterStore)nclass Dialog extends React.Component<{ isOpen: boolean }> {n public render() {n return (n <div style={{display: this.props.isOpen ? "block" : "none"}}>省略</div>n )n }nn public close () {n dispatch(new ChangeDialogState(false))n }n}n
在其他組件打開這個對話框:
dispatch(new ChangeDialogState(true))n
connect 函數可以給組件指定一個臨時的 store,僅僅用來做狀態提升。組件初始化的時候 mount 到 system 中抓取 ChangeDialogState 消息。當組件被卸載的時候,這個 store 也同時會從 system 中卸載。
2、生命周期
是的沒錯,store 居然還能帶生命周期。目前有隻 preStart 和 postStop 兩個生命周期函數。下面是一個通過繼承的方式,用這兩個函數實現一個列印日誌的中間件。
import { Store } from "ractor"nnexport class LoggerStore extends Store<{}> {n public loggerListener = (obj: object) => {n const date = new Date()n console.log(`${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}:`, obj)n }nn public preStart() {n // 自定義一個列印日誌的功能n // store 啟動的時候監聽系統事件中心n this.context.system.eventStream.on("*", this.loggerListener)n }nn public postStop() {n // store 停止的時候記得註銷監聽,防止內存泄露n this.context.system.eventStream.off("*", this.loggerListener)n }n public createReceive() {n return this.receiveBuilder().build()n }n}n
你也可以讓你的 store 主動觸發 action,比如 store 啟動之後主動載入緩存
class CacheStore extends Store {n preStart() {n dispatch(new GetByCache)n }n}n
3、catch 錯誤信息
Store 的內容實在很豐富不僅有生命周期,還能往 Store 的子節點生成 Store。當然還能 catch。能 catch 到 createReceive 返回的同步代碼錯誤。這一切都拜 js-actor 所賜
test("catch error message", t => {ntclass CatchActor extends AbstractActor {nttpublic createReceive() {ntttreturn this.receiveBuilder()ntttt.match(Entity, message => { throw Error("some error") })ntttt.build()ntt}nnttpublic postError(e: Error) {ntttt.is(e.message, "some error")ntt}nt}ntconst catchActor = system.actorOf(new CatchActor, "catchActor")ntcatchActor.tell(new Entity("hello"))tn})n
4、類型安全
這裡參考了 ng2 的依賴注入。import 進來了 Store,我們就能知道他的類型了。
// selector 的 state 的類型 ts 就能自動推導出來啦n@connect(CounterStore, state => ({}))n
5、依賴注入
因為是多 store,可以像 angular 注入 service 一樣注入我們的 store service。
ractor-angular
@NgModule({n imports: [n BrowserModule,n StoreModule.provideStore(AppStore)n ],n declarations: [n AppComponentn ],n bootstrap: [AppComponent]n})nexport class AppModule { }nnclass Counter {n constructor(public appStore: AppStore) { }n}n
ractor-react
@Providers([n { provide: TodoStore },n { provide: OtherStore, selector: state => ({data: state.data}) }n])n
ractor 繼承了上面提到的 redux 的優點的同時,又用自己的方式解決了 redux 的部分缺點。其實就是 redux 的改良方案,借用了 actor 模型的寫法。
用一句話來總結我的方案的話:一個帶生命周期,能catch,能繼承,能動態載入的 reducer。
一個多個子項目的狀態設計
那 redux 官方的例子:Isolating Subapps
import React, { Component } from reactnnclass Portal extends Component {n render() {n return (n <div>n <A/>n <B/>n <C/>n </div>n )n }n}n
這是一個企業級應用,入口是 portal 項目,portal 這個項目下面還有3個子項目 A, B, C
現在有2種場景:
場景一:
a,b,c 和 portal 之間互不干擾,沒有共享狀態,沒有共享 action 。但是一些信息,比如登錄信息這種共享信息存在 portal,也就是說 a,b,c 依賴 portal 的狀態。
這個場景下 redux 是沒辦法的。因為涉及到共享,狀態必須在同一個 event system 中進行交換,也就是說 action 也必須在同一個 event system 中進行傳播。而 redux 的 event system 等於 store。是單 store的,所以這個場景下在完全不通知 portal 我有什麼 action 和 reducer 的情況下,只能通過 props 傳遞了。
但是 ractor 強調所謂的"單"指的是 event system,store 可以獨立。所以a,b,c直接使用portal 的 system,把私有的 store 掛載到 portal 的 system。為了防止命名空間衝突,ractor 已有的特性中還支持把 store mount to store(特性已實現,寫法還沒支持). 所以結果顯而易見。
場景二:
a,b,c 和 portal 之間互不干擾,沒有共享狀態,沒有共享 action 。也沒有場景一的依賴portal,是完全獨立的項目。
恩,這個場景 redux 和 ractor 都可以很好的處理:
redux
import React, { Component } from reactnimport { Provider } from react-reduxnimport { createStore } from reduxnimport reducer from ./reducersnimport App from ./Appnnclass SubApp extends Component {n constructor(props) {n super(props)n this.store = createStore(reducer)n }nn render() {n return (n <Provider store={this.store}>n <App />n </Provider>n )n }n}n
ractor
import React, { Component } from reactnimport { System} from ractor-reactnimport TodoStore from ./todo.storenimport App from ./Appnnclass SubApp extends Component {n constructor(props) {n super(props)n this.system= new System()n }nn render() {n return (n <Provider system={this.system} stores={[]}>n <App />n </Provider>n )n }n}n
為 Provider 提供單獨的 system 就可以了。因為 event system 可以單獨指定,所以場景一的共享問題 ractor 才能很好的處理。(Provider 暫時沒有提供 system 屬性,之後加上)
多 store vs 單 store
我發現有人吐槽我的多 store 的設計是註定被單 store 的設計淘汰的,我這裡重新編輯下我的觀點。
我不認為 redux 的單 store 有缺點,只要事件匯流排是單的,那這個設計就是符合現在人對redux 單 store 的看法。我要反覆強調一點,我說我的方案的多 store 並不是 flux 的 store。我的 store 約等於 redux 里 reducer 在他領域裡扮演的角色。區別在於同樣是 event handler,redux 的建模(reducer)沒我的好(store)。也就是我的store比reducer更powerful(雖然我也是抄的 actor)。我說的我的方案的優點,redux 肯改的話也可以做到。我說的熱載入,redux 因此也可以做到。至於有沒有必要動態載入。我認為其實所有的狀態都是臨時的。說全局也是相對的。也就是說狀態臨時還是全局取決於創建者的慘遭物。我只需要給app提供一個event system,狀態們扮演好自己的角色就行了,比較像actor的字面意思。
最後
項目地址:ractor
發現對前端狀態管理感興趣的同學不少,方案還不完善。考慮了一下建了個群希望大家一起討論:618921336
推薦閱讀: