前端狀態管理與有限狀態機

樑仔發表於2018-10-15

當下前端流行的框架,都是用狀態來描述介面(state => view),可以說前端開發實際上就是在維護各種狀態(state),這已經成為目前前端開發的共識。

View = ViewModel(Model);
複製程式碼

理想情況下,ViewModel 是純函式,給定相同的 Model,產出相同的 View。

state => view 很好理解,但如何在 view 中合理地修改 state 也是一個問題。

為什麼需要狀態管理

舉個例子

圖書館的管理,原來是開放式的,所有人可以隨意進出書庫借書還書,如果人數不多,這種方式可以減少流程,增加效率,一旦人數變多就勢必造成混亂。

Flux 就像是給這個圖書館加上了一個管理員,所有借書還書的行為都需要委託管理員去做,管理員會規範對書庫的操作行為,也會記錄每個人的操作,減少混亂的現象。

一個比喻

我們寄一件東西的過程

沒有快遞時:

  • 打包準備好要送出去的東西
  • 直接到朋友家,把東西送給朋友
  • 很直接很方便,很費時間

有了快遞公司:

  • 打包準備好要送出去的東西
  • 到快遞公司,填寫物品,收件人等基本資訊
  • 快遞公司替你送物品到你的朋友家,我們的工作結束了

多了快遞公司,讓快遞公司給我們送快遞。

當我們只寄送物品給一個朋友,次數較少,物品又較少的時候,我們直接去朋友家就挺好的。但當我們要頻繁寄送給很多朋友很多商品的時候,問題就複雜了。

軟體工程的本質即是管理複雜度。使用狀態管理類框架會有一定的學習成本而且通常會把簡單的事情做複雜,但如果我們想做複雜一點的事情(同時寄很多物品到多個不同地址),對我們來說,快遞會讓複雜的事情變的簡單。

這同時也解釋了,是否需要新增狀態管理框架,我們可以根據自己的業務實際情況和技術團隊的偏好而新增,有些情況下,建立一個全域性物件就能解決很多問題。

核心思想

Flux 的核心思想:資料單向流動。

  • 不同元件的 state,存放在一個外部的、公共的 Store 上面。
  • 元件訂閱 Store 的不同部分。
  • 元件傳送(dispatch)動作(action),引發 Store 的更新。

Redux 的核心概念

  • 所有的狀態存放在 Store。元件每次重新渲染,都必須由狀態變化引起。
  • 使用者在 UI 上發出 action。
  • reducer 函式接收 action,然後根據當前的 state,計算出新的 state。

Redux store 是單一資料來源。Redux 沒有 dispatcher 的概念,轉而使用純函式(pure function)代替。

Redux store 是不可變的(Immutable)。

MobX

  • Observable:它的 state 是可被觀察的,無論是基本資料型別還是引用資料型別,都可以使用 MobX 的 (@)observable 來轉變為 observable value。
  • Reactions:它包含不同的概念,基於被觀察資料的更新導致某個計算值(computed values),或者是傳送網路請求以及更新檢視等,都屬於響應的範疇,這也是響應式程式設計(Reactive Programming)在 JavaScript 中的一個應用。
  • Actions:它相當於所有響應的源頭,例如使用者在檢視上的操作,或是某個網路請求的響應導致的被觀察資料的變更。

和 Redux 對單向資料流的嚴格規範不同,Mobx 只專注於從 store 到 view 的過程。在 Redux 中,資料的變更需要監聽,而 Mobx 的資料依賴是基於執行時的,這點和 Vuex 更為接近。

Vuex 的狀態管理模式

  • state,驅動應用的資料來源;
  • view,以宣告方式將 state 對映到檢視;
  • actions,響應在 view 上的使用者輸入導致的狀態變化。

Flux

Facebook 提出了 Flux 架構思想,規範了資料在應用中的流動方式。其基本架構如下入所示,其核心理念是單向資料流,它完善了 React 對應用狀態的管理。

前端狀態管理與有限狀態機

上圖描述了頁面的啟動和執行原理:

1.通過 dispatcher 派發 action,並利用 store 中的 action 處理邏輯更新狀態和 view

2.而 view 也可以觸發新的 action,從而進入新的步驟 1

其中的 action 是用於描述動作的簡單物件,通常通過使用者對 view 的操作產生,包括動作型別和動作所攜帶的所需引數,比如描述刪除列表項的 action:

{
    type: types.DELETE_ITEM,
    id: id
};
複製程式碼

而 dispatcher 用於對 action 進行分發,分發的目標就是註冊在 store 裡的事件處理函式:

dispatcher.register(function(action) {
  switch (action.type) {
    case "DELETE_ITEM":
      sotre.deleteItem(action.id); //更新狀態
      store.emitItemDeleted(); //通知檢視更新
      break;
    default:
    // no op
  }
});
複製程式碼

store 包含了應用的所有狀態和邏輯,它有點像傳統的 MVC 模型中的 model 層,但又與之有明顯的區別,store 包括的是一個應用特定功能的全部狀態和邏輯,它代表了應用的整個邏輯層;而不是像 Model 一樣包含的是資料庫中的一些記錄和與之對應的邏輯。

參考連結:flux

Redux

原生 Redux API 最簡單的用例

function counter(state, action) {
  if (typeof state === "undefined") {
    return 0;
  }

  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

var store = Redux.createStore(counter); //
var valueEl = document.getElementById("value");

function render() {
  valueEl.innerHTML = store.getState().toString();
}

render();
store.subscribe(render);

document.getElementById("increment").addEventListener("click", function() {
  store.dispatch({ type: "INCREMENT" });
});

document.getElementById("decrement").addEventListener("click", function() {
  store.dispatch({ type: "DECREMENT" });
});

document.getElementById("incrementIfOdd").addEventListener("click", function() {
  if (store.getState() % 2 !== 0) {
    store.dispatch({ type: "INCREMENT" });
  }
});

document.getElementById("incrementAsync").addEventListener("click", function() {
  setTimeout(function() {
    store.dispatch({ type: "INCREMENT" });
  }, 1000);
});
複製程式碼

應用中所有的 state 都以一個物件樹的形式儲存在一個單一的 store 中。 改變 state 的唯一辦法是觸發 action,一個描述發生什麼的物件。 為了描述 action 如何改變 state 樹,你需要編寫 reducers。

Redux 三大原則

  • 單一資料來源

    整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。

  • state 是隻讀的

    唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件。

  • 使用純函式來執行修改

    為了描述 action 如何改變 state tree ,你需要編寫 reducers。 改變 state 的惟一方法是 dispatch action。你也可以 subscribe 監聽 state 的變化,然後更新 UI。

嚴格的單向資料流是 Redux 架構的設計核心。

Redux API

Redux 的 API 非常少。

Redux 定義了一系列的約定(contract)來讓你來實現(例如 reducers),同時提供少量輔助函式來把這些約定整合到一起。

Redux 只關心如何管理 state。在實際的專案中,你還需要使用 UI 繫結庫如 react-redux。

  • createStore(reducer, [preloadedState], [enhancer])
  • combineReducers(reducers)
  • bindActionCreators(actionCreators, dispatch)
  • applyMiddleware(...middlewares)
  • compose(...functions)

immutable

在寫 redux 的 action 的時候,總是需要用到擴充套件語句或者 Object.assign()的方式來得到一個新的 state,這一點對於 JavaScript 而言是物件的淺拷貝,它對記憶體的開銷肯定是大於 mobX 中那樣直接操作物件屬性的方式大得多。

參考連結:redux-immutable seamless-immutable reselect 為什麼使用 Redux 管理狀態是可預測的

redux-saga

redux 是 react 技術棧中的狀態控制流框架,使用了標準的函式式思想,期望(強制)所有狀態管理都是純函式。這也意味著各狀態之間都是獨立的。但是有一類狀態 redux 直接甩給了的第三方模組,副作用模組 redux-saga 也就成了任勞任怨的典型代表。副作用正是因為不確定性和可變性而得名,而其給出的狀態又是相互影響,如何解耦使得原本複雜的非線性呈現為線性邏輯,正是有限狀態機的用武之處。

DvaJS

dva 首先是一個基於 reduxredux-saga 的資料流方案,然後為了簡化開發體驗,dva 還額外內建了 react-routerfetch,所以也可以理解為一個輕量級的應用框架。

在 redux 的生態圈內,每個環節有多種方案,比如 Data 可以是 immutable 或者 plain object,在你選了 immutable 之後,用 immutable.js 還是 seamless-immutable,以及是否用 redux-immutable 來輔助資料修改,都需要選擇。

參考連結:Redux 中文文件 immutable-js immer dvajs React + Redux 最佳實踐

MobX

MobX 是一個用法簡單優雅、同時具有可擴充套件性的狀態管理庫。

一個簡單的例子

import { observable, autorun } from "mobx";

const appState = observable({
  counter: 0,
  add(value) {
    this.counter += value;
  }
});

autorun(() => console.log(appState.counter));

setInterval(() => appState.add(1), 1000);
複製程式碼

在 mobx 中我們可以直接修改狀態

import { observable } from "mobx";

const appState = observable({ counter: 0 });

appState.counter += 1;
複製程式碼

可以通過引入 Strict 模式來避免這種不良好的實踐:

import { useStrict } from "mobx";

useStrict(true);
複製程式碼

MobX 脫胎於響應式程式設計(Reactive Programming),其核心思想為 Anything that can be derived from the application state, should be derived. Automatically,即避免任何的重複狀態。

MobX 中核心的概念即是 Observable,相信接觸過響應式程式設計的肯定非常熟悉,從後端的典型代表 RxJava 到 Android/iOS 開發中的各種響應式框架都各領風騷。

與 Redux 狀態管理上的異同

Redux / MobX 均為客戶端開源狀態管理庫,用狀態來描述 UI 介面,它們與 React 都不具有強繫結關係,你也可以配合別的框架來使用它們。 當然,與 React 是再合適不過的了,React 作為 View 層的框架,通過 Virtual DOM 機制來優化 UI 渲染,Redux / MobX 則提供了將相應狀態同步到 React 的機制。

Redux 與 MobX 的不同主要集中於以下幾點:

  • Redux 是單一資料來源,而 MobX 往往是多個 store。MobX 可以根據應用的 UI、資料或業務邏輯來組織 store,具體如何進行需要你自己進行權衡。
  • Redux store 使用普通的 JavaScript 物件結構,MobX 將常規 JavaScript 物件包裹,賦予 observable 的能力,通過隱式訂閱,自動跟蹤 observable 的變化。MobX 是觀察引用的,在跟蹤函式中(例如:computed value、reactions 等等),任何被引用的 observable 的屬性都會被記錄,一旦引用改變,MobX 將作出反應。注意,不在跟蹤函式中的屬性將不會被跟蹤,在非同步中訪問的屬性也不會被跟蹤。
  • Redux 的 state 是隻讀的,只能通過將之前的 state 與觸發的 action 結合,產生新的 state,因此是純淨的(pure)。而 MobX 的 state 即可讀又可寫,action 是非必須的,可以直接賦值改變,因此是不純淨的(Impure)。
  • Redux 需要你去規範化你的 state,Immutable 資料使 Reducer 在更新時需要將狀態樹的祖先資料進行復制和更新,新的物件會導致與之 connect 的所有 UI 元件都重複渲染。因此 Redux state 不建議進行深層巢狀,或者需要我們在元件中用 shouldComponentUpdate 優化。而 MobX 只自動更新你所關心的,不必擔心巢狀帶來的重渲染問題。
  • 在 Redux 中區分有 smart 元件與 dumb 元件,dumb 負責展示,smart 負責狀態更新,資料獲取。而在 MobX 中無需區分,都是 smart,當元件自身依賴的 observable 發生變化時,會作出響應。

Mobx 思想的實現原理

Mobx 最關鍵的函式在於 autoRun,autoRun 的專業名詞叫做依賴收集,也就是通過自然的使用,來收集依賴,當變數改變時,根據收集的依賴來判斷是否需要更新。Mobx 使用了 Object.defineProperty 攔截 getter 和 setter,和 Vue 一樣。

參考連結: mobx MobX 中文文件

Vuex

Vuex 是專門為 Vue.js 設計的狀態管理庫。把元件的共享狀態抽取出來,以一個全域性單例模式管理。它採用集中式儲存管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。

核心概念

  • State
  • Getter
  • Mutation
  • Action
  • Module

API

Vuex 的用法很簡單,  一句話總結:commit mutation,dispatch action

前端狀態管理與有限狀態機

參考連結:Vuex 官方文件

有限狀態機(FSM)

前端狀態管理與有限狀態機

有限狀態機(finite-state machine)又稱有限狀態自動機,簡稱狀態機,是表示有限個狀態以及在這些狀態之間的轉移和動作等行為的數學模型,非常有用,可以模擬世界上大部分事物。

有限狀態機並不是一個複雜的概念,簡單說,它有三個特徵:

  • 狀態總數(state)是有限的。
  • 任一時刻,只處在一種狀態之中。
  • 某種條件下,會從一種狀態轉變(transition)到另一種狀態。

總結

使用狀態去影響檢視,而 Action 主要負責完成狀態間的變更。程式碼如何更好的構建其核心在於使用最合理的狀態去管理介面,並用最合理的動作去實現狀態間的變更。

所謂的狀態管理,實際上就是使用有限狀態機來管理前端狀態。

有限狀態機對 JavaScript 的意義在於,很多物件可以寫成有限狀態機。

寫程式碼之前,思考一下:

  • 頁面有幾種狀態(初始化狀態?成功狀態?失敗狀態?出錯狀態?)。
  • 描述這些狀態需要什麼引數。
  • 在什麼時候轉變狀態,需要改變哪些部分。

然後跟著思路,完成資料與 UI 部分。

參考連結:javascript-state-machine xstate managing-state-in-javascript-with-state-machines-stent

相關文章