從人類行為的角度理解狀態管理
人從出生到死亡走的這段路程,稱為生命週期。
應用從啟動到關閉經歷的這段過程,也稱為生命週期——因此這是一個仿生概念,基於相應結構的應用也會有與人類相似的行為特點。
初接觸時,我們會為如何去更好的在這個過程中去實踐 狀態管理 焦頭爛額,糾結於不同架構的各個節點對應的職責,特別是在涉及非同步和副作用的處理的過程中,很難快速找到一個“最佳實踐”。那麼,既然應用具有仿生設計,我們自然可以從基於作為人類的自身的角度去理解它。
狀態(State)
先來看一張圖:

從上圖可以看到,如果將一些過程和行為抽象出來,人與 App 是具有高度的相似性的。
其中:
Stage | Human | App |
---|---|---|
Born/Startup | 新生兒出現,有了人類初始特徵,大腦開始工作後,便逐漸的產生 本能、認知、意識、反應、情緒 等元素,它們便是我們的 初始狀態 。 | 註冊程序或執行緒,靜態資源載入,支撐應用的各個要素就緒,根據快取或預置規則定義應用的 初始狀態 |
EE/FID | 早期教育,為投身到更復雜的環境層層遞進的準備 | 獲取初始資料,為更定製化的執行策略做準備 |
WL/RI | 不斷學習、工作、修身、社交, 處理事件 | 在執行過程中與後臺互動、與使用者互動、檢視與狀態的同步、與其他應用的互動, 處理事件 |
Retirement/Unmount | 退休、處理各項工作時的羈絆、夕陽紅、遺產處理 | 解除安裝服務、處理副作用、也啟動一些服務(最令人髮指的)、快取等資源的處理 |
從書面意思理解:
狀態是人或事物表現出來的形態。是指現實(或虛擬)事物處於生成、生存、發展、消亡時期或各轉化臨界點時的形態或事物態勢。
簡單來說,就是 任一物件在特定狀況下的存在形態 。 具體到人,可以是情緒、職業、資產等。 具體到應用,可以是一些布林值、狀態碼、具體資料等。
由這些可以對物件進行描述的單元結合,就構成人或應用。
全域性狀態(Global State)
- 在人類的角度來看,我們剛形成胚胎便會有了 性別、膚色、瞳色 等基因決定的特徵,我們因而為人,這些生理特徵體現在我們生命歷程中的任何一個時刻。
- 對於應用來說,靜態資源被執行環境執行的過程,就好比胚胎的生長過程,然後到了初始化狀態容器(Store)的時候,便開始獲取初始資料,這些資料就包含了一系列初始狀態,它們可以是 可用性、登入狀態、角色策略、顏色主題、語言環境 等等。
在人身上,本能、認知等因素往往是伴隨一生的,他們的有效性是覆蓋到所有其他情形下的,比如我吃飯的時候不知道美國總統是特朗普,那麼我上廁所睡覺的時候同樣不會知道;有一天我在吃飯的時候得知了這個訊息,那麼從此我上廁所睡覺的時候同樣也知道了。
在應用中,登入狀態、顏色主題、語言環境等也有這樣的特點。在一個應用週期內,每一次修改這些狀態,都是會應用到全域性的,至少是主體同步。
對於這些狀態,我們統稱為 全域性狀態
區域性狀態(Module/Feature/Partial)
我們上廁所的時候,一般會向”抽紙盒“發起請求,然後拿幾張紙,這是在如廁時的”後事“預備狀態;而我們在大街上則不會同樣拿著紙準備擦屁股,我們可能因為口渴拿著水,因為購物拿著包袋。
在應用裡,具有不同職責的頁面展示的內容也不同,我們不會沒事兒在首頁展示使用者的優惠券詳情,也不會沒事兒在課程詳情頁展示使用者餘額。
在這些具有不同職責的場景下“獨有”的狀態,我們稱為 區域性狀態
全域性狀態和區域性狀態的劃分
兩種狀態的特點其實很好理解,其實它們的劃分才是難點。
狀態本身就具有“全域性性”,因為狀態一旦拿到,那麼不管是否是在對應場景,它都存在於本次生命週期中,它隨時可能在新需求來到的時候被其他場景需要,而你不一定總是能夠事先知曉。
當然了,像使用者資訊、登入時間、語言環境等因素是很好區分的,但更多更細的狀態是否需要放到全域性,或者說由公共性更高的模組來管理,就很難一次性下定論了。
因此界定某個狀態的型別,並不是一蹴而就的,而是要在長期的迭代中進行總結。
對於人來說,這個問題不算是個問題,因為我們擁有強大的複雜問題處理能力,而計算機幾乎是沒有這樣的能力的,它們處理問題的方式都是人為定義的,即便 AI、ML 等技術蓬勃如今,也遠遠達不到人類的思維水平。
我們以一個真實應用裡的一些實現為例:

產品的移動端 Web App。
以職責劃分
第一張圖中,在兩個功能不同的 頁面(場景) 中都出現了 分類 這一資料形式,並且資料是一致的,也就是說,這兩個頁面出現了公共狀態。而這兩個公共狀態總是覆蓋全域性的,那麼我們就應當將它們提升到更高一層的狀態模組中。
注意,提升到更高一層並不意味著提升到 global 的級別。有時候,可能分類並不是一個簡單的資料,它可能是根據不同的使用者策略進行展示的,對於不同的使用者級別,分類可能會呈現多型(比如普通使用者看不到 VIP 專屬的類別)。從前後端互動的角度來看,分類相關的介面往往也是獨立於其他資料的。因此,當分類具有了一定的複雜性和具體規則,它應當有屬於自己的管理單元,使得資料的吞吐和處理有更加清晰的思路,而不是去破壞性的影響全域性狀態的職責。
就像人類在左右腦的統一調配下,有視覺中樞、聽覺中樞、運動中樞。如果它們產生了紊亂,使得腦功能失調,人就會出現各種各樣的問題,如少兒多動症、認知障礙等。
以路由劃分
為什麼是一個圓圈呢?
其實這個頁面雖然常見,但在狀態管理中,它的確比較特殊,因為它既是一個路由單元,又是一個狀態單元。如果說一個頁面通常是由多個狀態組合而成的,那麼“我的”頁面可能就只需要一個狀態就夠了——即使用者狀態。它往往包含了使用者基本資訊,信用卡資訊,功能定製等——是的,往往我們就把它們放在 global 中。當然根據應用的型別和複雜度不同,使用者資訊也可能劃分成若干單元,因此,如此形式的 按路由劃分 ,其實是 按職責劃分 的一個變種。
然而,還有一種情況就不同了。比如某些活動型頁面,它們可能只包含一些運營內容,有一套自己的邏輯和互動,獨立於任何其他的頁面,但頁面本身的生命週期或許只有幾個星期甚至幾天,這時候可能就沒有必要為其設計和維護一個狀態單元了,得不償失。
好比一個人要出國旅遊一段時間,立馬給手機開了一系列便捷的境外服務,但回國後往往就立馬停掉了,而不是為這些長期用不到的東西付費。
改變狀態
我們已經探討了關於 狀態 的一些基礎內容,現在問題來到了如何對狀態進行“改查”。
首先,狀態是 物件 在 特定環境 和 具體時機 下的某種存在表現,隨著環境和時機的改變,它便會發生相應的更新。
我們拿“時間”舉例,它是最客觀最不可阻擋的狀態流。
對於人類,在一個時間單元內(指,年、月、日等)我們會根據具體的時間點調整我們自身的狀態——睡覺、起床、工作、小憩等等;
對於應用,最常見的就是一些即時服務的開關。比如某購物 App,白天一直到晚上九點會有針對會員的“一小時送達”的服務,但過了這個時間點,這個服務便進入休眠狀態。
那麼從外部條件改變到物件自身的狀態更改,中間經歷了什麼呢?
我們來看幾個當下炙手可熱的前端資料流模型:

So You See!
這裡面似乎有一個恆定的正規化:
Action - Update state
Store 和 State
Store
是狀態中心,而 State
就是這裡面的一個個狀態集合。
在 Flux 和 Redux 的模型中,我們可以顯式的看到 Store
節點,而 Mobx 和 Vuex 裡這個節點似乎由 State
代替了。這個是由於兩種風格不同的狀態宣告方式導致的,這裡以最常見的 Redux 和 Vuex 的 Store
構建方式為例:

可以看到,源於 Flux 思想的 Redux 的 Store
宣告過程更像是將各個獨立的狀態單元(Reducer,詳見後文)整合(combine)在一起,形成一個自 Store
而下的狀態樹,實現單向資料流。其工作特點是所有的行為都要經過 Store
。 PS :其中的 Action Handlers
的實際形式其實是 switch
語法下的一個個模式,並非具體函式或方法,這裡只是根據其職責進行了類比理解。
而 Vuex 呢,其實也是源於 Flux 的,但它吸取了 Redux 樣板程式碼繁瑣的“教訓”,將 combine
的過程用宣告的方式規避了,同時將狀態單元細分成各個 module
,每個 module
包含了一套 State
和對應的規則,比起 Redux 來說,是一種“高類聚、低耦合”的方案,節省了一些宣告和管理狀態的成本。工作特點眼下就是各個 module
各司其職,隻影響自己的 State
。
然而,Vuex 在實際的工作過程中,其實還是由 Store
作為中心進行分發,只是其構建方式讓我們覺得 Store
並沒有被總是調起。
總的來說, Store
的地位如同我們的大腦,我們的任何決策、行為都會經過大腦進行評估、加工。但隨著某種刺激的不斷觸發,其對應的反應行為也會出現得越來越快,等到形成相對固定的正規化的時候,我們可能就感覺不到思維在這個過程中的行動了,體現為“反應快”。 對於普通應用開發來說,我們則可以直接定義這種“正規化”,這更有利於我們整體上的把握應用的規則,強化和優化應用的邏輯。良好的狀態管理實踐會讓應用更加高效,也更好維護。
Action
在上面的幾種資料流模型中,在對狀態進行修改前,都會經過一個叫 Action 的節點,這個節點我們可以理解成 行為 。
Action 即是向 Store
發起更新請求的最小單元。
它的結構通常是:
// pureObject const myAction = { type: 'GO_TO_BED', payload: Medicine.Estazolam } // functional const myFunctionalAction = arg => { let payload // TODO return { type: 'GO_TO_BED', payload } } 複製程式碼
其中:
- type: 對這個行為的描述,Store 根據這個欄位去尋找對應的處理方案
- payload?:荷載,攜帶實現該行為要使用的一些資料
這個比較好理解,要做一件事,得先明確這是什麼事,如果有需要還要帶上相應的東西。比如:大便要帶紙;而小便可能帶,也可能不帶;只是去洗手就什麼都不用帶了。
可見, Action
最終只是一個物件,那它如何傳遞給 Store
呢?
Dispatch
我們完成一個“刺激——反應”的時候,通常先是神經末梢收到接收刺激,然後大腦得到神經末梢發來的資訊,做出反應。在這個過程中,攜帶資訊的介質被稱為神經遞質,它活動在突觸之間。
而在各類應用狀態管理的模型中,通常都會有一個 dispatch
方法,它就宣告在 Store
上,負責呼叫各個 Action
,然後由 Store
上對應的分發機制進行處理。同時,非同步 Action
的實現,即是將這個方法作為引數傳給對應的 ActionCreator
,然後等到非同步工作流完成後,將最終的 Action
傳遞給 Store
。例如構建一個 redux-thunk
中的非同步 Action
:
const asyncAction = id => { // 整合 redux-thunk 後,redux 會將 dispatch 等一系列方法傳遞給 actionCreator 返回的函式,供非同步工作完成後 actionCreator 能配合 Redux 進行工作 return dispatch => { fetch('/getData?id=' + id) .then(response => response.json()) .then(data => { dispatch({ type: 'SET_DATA', payload: data }) }) } } 複製程式碼
Reducer / Mutation
現在到了更新狀態的時候了,簡單抽象出來就是 newState = updatedState
,不難理解,主要看下實現。
在 Flux 和 Mobx 的模型中,對狀態的修改比較直接,不多贅述,那麼“矯情”一些的 Redux 和 Vuex 是如何實踐的呢。
我們從其實現上分別說明它們的作用
Reducer
先來看一個簡單 Reducer 實現:
function myReducer (state = { age: 1 }, action) { switch (action.type) { case 'HappyBirthDay': return { age: ++state.age } default: return Object.assign({}, state) } } 複製程式碼
Reducer 的工作方式是,接收一個 Action
,然後在 switch 流中匹配 action.type
,做出相應處理,然後返回一個 新的物件 。其原始碼可以看 這裡
為什麼是新的物件呢?
因為 Redux 是一個實踐 函數語言程式設計(FP) 理念的庫。函數語言程式設計有個要素就是——純函式不能有 副作用 ,而副作用簡單概括來說就是 對該函式內部環境以外的變數進行了修改、銷燬等操作 。
回過頭來,在 Redux 中,Reducer 原則上就是一個純函式。
這有什麼意義呢?
答案是 資料不可變 ,它也是函數語言程式設計中的一個要點。
函數語言程式設計認為 可變 和 共享 是“萬惡之源”,原資料的更新只能通過返回新的資料。否則隨意修改的資料可能讓應用產生難以預料的問題,而“共享”加“可變”帶來的副作用更是容易容易讓我們得到錯誤並且難以捕獲的內容。
Mutaion
Vuex 是在 Mutation
中修改狀態的,其程式碼一般如下:
// module export default { //... mutations: { SET_DATA (state, payload) { state.data = payload } }, actions: { async getData ({ commit }, payload) { const res = await api.data.get(payload.id)() commit('SET_DATA', res.data) } } } // component export default { mounted () { store.dispatch('getData', this.id) } } 複製程式碼
其中,觸發一個 Action
依然是通過 dispatch
方法,然而,修改狀態為什麼需要 commit
一下呢?
其實我們直須將 mutations
和 actions
中的各個成員都理解成 Action
,因為你也可以直接在 Store
上呼叫 commit
來修改狀態。 commit
的職責相當簡單,就是修改本地狀態。
而 Action
的職責在於可以實現非同步流和 Action流 (在action中dispatch又一個(可以是自己)action),最後提交到 Mutation
中來修改狀態。但從原始碼來看,其實 Vuex 同樣賦予了 Action
改變狀態的能力,它將 State
作為第一個引數的其中一個屬性傳遞給了 Action
,其目的是為了你可以使用 State
上的狀態和資料,原則上這是隻讀的,但結合 MVVM 的特點,你在這裡修改它,同樣也會引起檢視的改變。
原始碼裡是這樣寫的:
function registerMutation (store, type, handler, local) { const entry = store._mutations[type] || (store._mutations[type] = []) entry.push(function wrappedMutationHandler (payload) { // store 會呼叫 commit 方法來啟用這個 mutation,並且只傳入了本地 state 和一系列荷載 handler.call(store, local.state, payload) }) } function registerAction (store, type, handler, local) { const entry = store._actions[type] || (store._actions[type] = []) entry.push(function wrappedActionHandler (payload, cb) { // action 就比較厲害了,這麼多... let res = handler.call(store, { dispatch: local.dispatch, commit: local.commit, getters: local.getters, state: local.state, rootGetters: store.getters, rootState: store.state }, payload, cb) if (!isPromise(res)) { res = Promise.resolve(res) } if (store._devtoolHook) { return res.catch(err => { store._devtoolHook.emit('vuex:error', err) throw err }) } else { return res } }) } 複製程式碼
可見,你甚至可以不顧一切的在 Action
中修改全域性的 State
。
在行為心理學中,其中一種行為的分類方式即是將行為分為 外顯行為 和 內隱行為 。外顯行為就是我們肉眼可見的,有明確外在表現的行為;內隱行為則是外表之下,發生於機體內部的情緒變化、思維運作、激素分泌等不會彰顯出來的行為。但往往我們改變大腦中的某個狀態,使之顯於或不顯於我們的姿態的時候,這些內隱行為是不可能避免的,因為我們的大腦活動就是各類遞質工作下的一系列的化學反應。
這與 Action
和 Mutation
的關係很像,Vuex 告訴我們修改狀態的唯一方法是提交 Mutation
,也就是說你不應該在 Action
中的直接修改 State
,就好像我們的外顯行為總是要經過內隱行為來提交給大腦一樣。
當然了,既然職責不同,角色肯定就不同,理解成 Action
是為了我們便於理解。
再次提醒,Vuex 明確告訴我們改變狀態的唯一方法是提交 Mutation
,因此我們應當遵循這個原則,將 Action
中的各個響應式引用視為只讀,以保證應用的邏輯性不會被破壞。(當然,直接 commit 啦,想想 mapMutations
方法!)
總結
通過一張圖來梳理一下狀態管理與人類行為的共通之處:

PS
這不是一篇論述,狀態管理也遠遠不止這幾種模型,小生僅僅在 前端應用 及其比較 有代表性的狀態管理方案 的背景下分享了這個角度,因此這個理解方式必然有一定的侷限性或者是未被完全論證。如果能幫助到讀者,小生就非常榮幸了。
理解狀態管理的方式有很多,這只是其中一種思路,或許這種思路能在應用開發的同時也鍛鍊我們的邏輯思維。
同時,實際場景下的狀態管理必然一個更加複雜的東西,隨著應用的規模和深度越來越大,我們需要更深刻思考它,如何劃分模組?如何共享模組?如何構建容器?如何提升效率?這都是需要逐步探索的,當然,最快的方式,就是在已有的狀態管理正規化中思考,組織、優化。
最後,要記住的是:
你可能不需要狀態管理