前端:從狀態管理到有限狀態機的思考
1. 狀態管理
在我們前端開發中,一定會接觸現在最熱門的幾大框架(Vue, React等等),在使用框架的過程中,我們一定會接觸某些狀態管理工具。
Vue我們會使用Vuex來管理全局狀態, React會使用Redux來管理。
首先是不是,在問為什么?
在使用類似Vue,React框架時,我們一定會使用狀態管理嗎?這個答案是肯定的。或許我不會主動去使用Vuex, Redux,但我們編寫每一個組件的時候就已經在管理狀態,Vuex, Redux只是更方便我們進行全局的狀態管理。
為什么一定會使用狀態管理?這是因為現代前端框架使用數據驅動視圖的形式來描述頁面。比如,Vue、 React組件會有一個自己內部,外部的狀態來共同決定組件的如何顯示的,用戶與組件交互導致數據變更,進而改變視圖。
框架 | 內部狀態 | 外部狀態 |
---|---|---|
Vue | data | props |
React | state, useState | props |
所以我們所寫大部分業務邏輯,是在管理狀態,框架會幫我們狀態映射成視圖,這可以說是很經典的MVVM模式。
- View = ViewModel(Model);
- // 視圖 = 狀態 + 管理
- 復制代碼
2. 有限狀態機:計算機中一種用來進行對象行為建模的工具
其作用主要是描述對象在它的生命周期內所經歷的狀態序列,以及如何響應來自外界的各種事件。
我們來理解一下上面這段話。
-
一種對象行為建模工具
我們用來描述對象行為,狀態隨著時間轉變過渡行為的工具。可以模擬世界上大部分事物。
-
生命周期
我們通過抽象對象所經歷的狀態序列,來確定對象一系列可能的生命周期和轉變。
-
響應外界事件
外界事件能夠影響對象內部狀態。對象能夠對外部事件作出響應。
狀態機有基本幾個要素:
-
當前所處狀態
在各個時刻只處于一種狀態
-
狀態轉移函數
在某種條件下,會從一種狀態轉移到另外一種狀態。
-
有限狀態序列
擁有有限,可枚舉的狀態數
上面這張圖所描述的狀態機,我們使用js對象來進行描述
- const stateTool = {
- // 當前狀態
- currentState: '1',
- // 狀態轉變函數
- transition: (event) => {
- switch(event.type) {
- case '1': {
- this.currentState = event.status;
- doSomething1();
- break;
- }
- case '2': {
- this.currentState = event.status;
- doSomething2();
- break;
- }
- case '3': {
- this.currentState = event.status;
- doSomething3();
- break;
- }
- default:
- console.log('Invalid State!');
- break;
- }
- }
- }
- 復制代碼
使用有限自動機是一種狀態管理的思考方式,我們可以列舉組件狀態列表,設計觸發狀態函數。通過外部或內部交互行為,觸發函數改變狀態,根據狀態改變視圖
3. Flux思想
Flux是什么?Flux是一個Facebook開發的、利用單向數據流實現的應用架構
簡單說,Flux 是一種架構思想,專門解決軟件的結構問題。可以說他是有限狀態機的另外一種形式。
一個Flux管理分為4個狀態:
-
View:視圖層
-
Action(動作):視圖層觸發的動作 or 行為
-
Dispatcher(派發器):收集觸發行為,進行統一管理,統一分發給store層。
-
Store(數據層):用來存放應用的狀態,根據dispatcher觸發行為,就提醒Views要更新頁面
-
初始狀態
我們通過store 存放的是初始化狀態,這種初始化狀態數據可以頁面初始化時設定 或 頁面加載時請求后端接口數據,來初始化store數據。
通過store的初始化數據,來構建初始化的視圖層。
-
狀態轉移事件
根據視圖層的行為會觸發action,我們通過統一的dispatcher來收集action, dispatcher將行為派發給store。
-
狀態轉移函數
store通過判斷事件的類型 和 payload,來修改內部存儲狀態。達到狀態轉移的目的,并統一提醒view層更新頁面;
4. 全局到局部的狀態管理
既然我們是通過數據狀態來管理視圖的,那么在設計初期我們就可以從有限的狀態轉移來思考業務邏輯。通過思考每個狀態對應的數據,狀態轉移函數,我們可以很清晰的羅列出數據更變邏輯。從數據去控制視圖也是現代前端所接觸到的MVVM模式。
一個大型應用,我們也會使用Vuex 或 Redux來進行一整個應用的管理。
在平時的業務中,我們會遇到一個痛點是:Vuex,Redux是一個全局狀態管理,但我們現在需要在局部需要一個局部狀態管理變更,只能使用 mutation
或 dispatch
去提交更改。
如果我們頻繁的更新狀態,那么我們需要為每一個局部模塊編寫大量dispatch函數來間接修改全局狀態。隨著應用的擴充,dispatch文件會越來越臃腫。
那么我們是不是可以使用不同的狀態管理工具,來實現局部狀態的管理。在局部狀態更新完之后,再去用局部更新去更新全局呢?
注:但這也會有一個缺點,局部管理相對獨立。有些高度復用的提交函數需要放在全局狀態管理上
a. 框架原生組件狀態管理
React Hooks + React.createContext
React Hooks提供了useReducer + useContext + Context 可以實現一個小型的狀態管理
- // 以下代碼就實現了一個能夠穿透組件的狀態管理
- import React, { useReducer, useContext } from 'react';
- const reducer = (state = 0, { type, ...payload }) => {
- switch (type) {
- case 'add':
- return state + 1;
- case 'desc':
- return state - 1;
- default:
- return state;
- }
- }
- const Context = React.createContext();
- const Parent = () => {
- const [state, dispatch] = useReducer(reducer, 0);
- return (
- <>
- <Context.Provider value={{ state, dispatch }}>
- <Son />
- </Context.Provider>
- </>
- )
- }
- function Son() {
- return <Counter />
- }
- function Counter() {
- const { state, dispatch } = useContext(Context);
- return (
- <div>
- <button onClick={() => dispatch({ type: 'desc' })}>-</button>
- {state}
- <button onClick={() => dispatch({ type: 'add' })}>+</button>
- </div>
- )
- }
- export default Parent;
- 復制代碼
Vue響應式數據 + vue.Provide/inject
使用vue響應式系統 + provide/inject API來實現一個具有穿透性的局部狀態管理
- // Parent.vue
- <template>
- <Son />
- </template>
- <script setup>
- import { provide, reactive, readonly } from "vue";
- import Son from "./Son.vue";
- const data = reactive({
- count: 0,
- });
- const onAdd = () => {
- data.count++;
- };
- const onDesc = () => {
- data.count--;
- };
- provide("store", {
- data: readonly(data), // 只讀屬性
- onAdd, // 修改函數add
- onDesc, // 修改函數desc
- });
- </script>
- 復制代碼
- // Son.vue
- <template>
- <Counter />
- </template>
- <script setup>
- import Counter from "./Counter.vue";
- </script>
- 復制代碼
- // Counter.vue
- <template>
- <div>
- <button @click="store.onDesc">-</button>
- {{ store.data.count }}
- <button @click="store.onAdd">+</button>
- </div>
- </template>
- <script setup>
- import { inject } from "vue";
- const store = inject("store", {}); // 穿透讀取store
- </script>
- 復制代碼
b. 線性狀態管理:Xstate
Xstate是一個很有趣的類似有限狀態機的狀態管理, Xstate
著重點在于 管理狀態 ,通過 狀態轉換去維護數據 。
我們來定義一個簡單的promise狀態機,使用官方提供的工具進行可視化
- import { Machine } from 'xstate';
- // 創建狀態機
- const promiseMachine = Machine({
- id: 'promise', // 唯一id
- initial: 'pending', // 初始化狀態
- states: { // 狀態集合
- pending: {
- on: {
- RESOLVE: 'resolved',
- REJECT: 'rejected',
- }
- },
- resolved: {
- type: 'final',
- },
- rejected: {
- type: 'final'
- }
- }
- })
- 復制代碼
注意:warning::狀態機不擁有狀態,他只是定義狀態和定義狀態轉移
Xstate有提供函數來實現狀態機服務,實現擁有狀態的實體
- import { interpret } from 'xstate'
- const promiseService = interpret(promiseMachine).onTransition(state =>
- console.log(state.value)
- ) // 創建服務,指定狀態轉移時回調函數
- promiseService.start() // 啟動服務
- promiseService.send('RESOLVE'); // 通知服務轉移狀態,并執行回調函數
- 復制代碼
這樣子我們就實現了一個簡單的Promise狀態機。他有很多應用,可以結合Vue,結合React進行使用。更加深入的內容就需要到官方文檔中自行探索了!
就我個人的看法,狀態機思想非常適合狀態轉移相對線形的場景,在某些狀態多循環的場景轉移會相對復雜些
c. 可響應式的狀態管理器:Mobx
mobx是一種響應式的狀態管理,他所提倡的是拆分store做數據管理。這就很適合做局部的狀態管理,根據局部狀態管理來更新全局狀態。
相同的,我們舉個例子
- import { action, autorun, observable } from 'mobx'
- import { observer } from 'mobx-react'
- import React from 'react'
- const appStore = observable({ // 建立store
- count: 0,
- age: 18,
- })
- // autorun 只會觀察依賴的相關數據
- // 使用當appStore.age更新時,才會觸發該函數
- autorun(() => {
- // doSomething();
- console.log('autorun', appStore.age);
- })
- const Counter = observer(() => {
- const { count } = appStore;
- const onAdd = action(() => { // 使用action更新store數據
- appStore.count++;
- })
- const onDesc = action(() => {
- appStore.count--;
- })
- return (
- <div>
- <button onClick={onDesc}>-</button>
- {count}
- <button onClick={onAdd}>+</button>
- </div>
- )
- })
- export default Counter;
- 復制代碼
5. 總結
現在前端主流使用數據驅動視圖的形式,來實現業務。希望給大家帶來兩點啟發
-
用有限狀態機去思考某些線性狀態場景的數據管理。
-
在之前的業務開發的時候,就會出現一個痛點,應用全局狀態管理非常臃腫。
在不斷功能迭代的過程中,需要做不同的狀態管理,雖然都是對同一份數據進行維護,但維護的方式不同,進行一次狀態更新就需要編寫一個不同的dispatch函數。隨著業務需求的增加,dispatch函數越來越多,難以管理和復用。
思考如何解決這個問題的時,偶然看到了有限狀態機相關文章,思考到應用的功能模塊在某一個時刻是相互獨立的,我們在局部將數據進行更新,之后用一個全局函數對數據進行統一替換。
注:本文為探索性質,使用原生組件進行局部管理不需要引入依賴。但使用第三方工具造成包體積大小的增加,是否會增加性能消耗有待討論