最近我開始思考React應用的狀態管理。我已經取得一些有趣的結論,而且在這篇文章裏我會向你展現咱們所謂的狀態管理並非真的在管理狀態。javascript
譯者:阿里雲前端-也樹前端
原文連接:managing-state-in-javascript-with-state-machines-stentjava
咱們來看一個簡單的例子。想象這是一個展現用戶名稱、密碼和一個按鈕的表單組件。用戶會在填寫表單後點擊提交。若是一切順利,咱們完成了登陸,而且有必要展現歡迎信息和一些連接:web
咱們假定這個組件有兩個展現狀態。一個是未登陸狀態,另外一個是用戶登陸後的狀態。因此從管理這兩種狀態開始,咱們用一個布爾值的標誌位來描述用戶的狀態。後端
var isLoggedIn; isLoggedIn = false; // 展現表單 isLoggedIn = true; // 展現歡迎信息和連接
可是這樣還不夠。若是咱們點擊提交按鈕後觸發的HTTP請求須要一些時間來響應,咱們不能把表單孤零零的放在屏幕上,而須要更多的UI元素來展現這樣的中間狀態,所以咱們不得不在組件中引入另外一個狀態。安全
如今咱們有了第三種展現狀態,僅僅用一個 isLoggedIn
變量已經不能解決了。不走運的是咱們不能設置變量值爲 false-ish
,它不是 true
也不是 false
。固然,咱們能夠引入另外一個變量好比說 isInProgress
。一旦咱們發送請求就會把這個變量的值置爲 true
。這個變量會告訴咱們是處於請求的過程當中而且用戶應該看到加載中的展現狀態。服務器
var isLoggedIn; var isInProgress; // 展現表單 isLoggedIn = false; isInProgress = false; // 請求過程當中 isLoggedIn = false; isInProgress = true; // 展現歡迎信息和連接 isLoggedIn = true; isInProgress = false;
很是棒!咱們用到兩個變量而且須要記住這三種狀況對應的變量值。看起來咱們解決了問題。但另外的問題是,咱們維護了太多狀態。若是咱們須要展現一個請求成功的信息,或者一切順利的時候咱們須要告知用戶:「Yep, 你成功登陸了」,而且兩秒後信息伴隨着華麗的動畫隱藏起來,接着展現出最終的界面,要怎麼辦?架構
如今狀況變得有些複雜。咱們有了 isLoggedIn
和 isInProgress
,可是看起來僅僅使用它們還不夠。isInProgress
在請求結束後確實是 false
,可是他的默認值一樣是 false
。我以爲咱們須要第三個變量 - isSuccessful
。函數
var isLoggedIn, isInProgress, isSuccessful; // 展現表單 isLoggedIn = false; isInProgress = false; isSuccessful = false; // 請求過程當中 isLoggedIn = false; isInProgress = true; isSuccessful = false; // 展現成功狀態 isLoggedIn = true; isInProgress = false; isSuccessful = true; // 展現歡迎信息和連接 isLoggedIn = true; isInProgress = false; isSuccessful = false;
咱們簡單的狀態管理一步步變成了由 if-else 組成的巨大的條件網,很難去理解和維護。工具
if (isInProgress) { // 請求過程當中 } else if (isLoggedIn) { if (isSuccessful) { // 展現請求成功信息 } else { // 展現歡迎信息和連接 } } else { // 等待輸入,展現表單 }
咱們還有一個問題會讓這個情景變得更糟:若是請求失敗咱們要怎麼作?咱們須要展現一個錯誤信息和一個重試連接,若是點擊重試咱們會重複一次請求的過程。
如今咱們的代碼已經沒有任何可維護性。咱們有很是多的場景須要知足,僅僅依賴引入新的變量是不可接受的。讓咱們想一想是否能夠經過更好的命名方式來解決,同時可能還須要引入一個新的條件聲明。
isInProgress
僅僅在請求的過程當中被用到。咱們如今還關心請求結束以後的過程。
isLoggedIn
有一點誤導的含義,由於咱們只要請求結束就把它置爲 true
。而若是請求出錯,用戶並無真正登入。因此咱們把它重命名爲 isRequestFinished
。雖然看起來好些了,可是它僅僅表明咱們從服務器得到了響應,並不能用它來判斷響應是否爲錯誤。
isSuccessful
是一個最終狀態合適的候選變量。若是請求出錯咱們能夠把它設置爲 false
,可是等等,它的默認值也是 false
。因此它也不能做爲表明錯誤狀態的變量。
咱們須要第四個變量,isFailed
怎麼樣?
var isRequestFinished, isInProgress, isSuccessful, isFailed; if (isInProgress) { // 請求過程當中 } else if (isRequestFinished) { if (isSuccessful) { // 展現請求成功信息 } else if (isFailed) { // 展現請求失敗信息和重試連接 } else { // 展現歡迎信息和連接 } } else { // 等待輸入,展現表單 }
這四個變量描述了一個看似簡單但實際並不簡單的過程,這個過程包含了許多邊界狀況。當項目進一步迭代時,最終可能會因爲已有變量的組合不能知足新的需求,而定義更多的變量。這就是構建用戶界面十分困難的緣由。
咱們須要更好的狀態管理方式。也許可使用更現代和更流行的概念。
最近我在思考 Flux 架構和 Redux 庫在狀態管理中的定位。即便這些工具和狀態管理有關,可是它們本質上不是解決這類問題的。
Flux 是 Facebook 用來構建客戶端 web 應用的架構。它利用單向數據流補足了 React 的視圖組件的組織方式。
Redux 是一個可預測的狀態容器,用來構建 JavaScript 應用。
它們是 「單向數據流」 和 「狀態容器」,而不是 「狀態管理」。Flux 和 Redux 背後的概念是很是實用和討巧的。我認爲它們是適合構建用戶界面的方式。單向數據流讓數據擁有可預測性,改進了前端開發。Redux 中的 reducer 擁有的不可變特性,提供了一種能夠減小 bug 的數據傳送方式。
就個人感覺來講,這些模式更適用於數據管理和數據流管理。它們提供了完善的 API 來交換改變咱們應用數據的信息,可是並不能解決咱們狀態管理的問題。這也由於這些問題是跟項目強相關的,問題的上下文取決於咱們正在作的事情。
固然像處理 HTTP 請求咱們能夠經過某個庫來解決,可是對其它相關的業務邏輯咱們仍然須要本身編寫代碼來實現。問題在於咱們如何用一種合適的方式去組織這些代碼,而不至於每兩年就把整個應用重寫一遍。
幾個月以前我開始尋找能夠解決狀態管理問題的模式,最終我發現了狀態機的概念。事實上咱們一直都在構建狀態機,只不過咱們不知道。
狀態機的數學定義是一個計算模型,個人理解是:狀態機就是保存你的狀態和狀態變化的一個盒子。這裏有一些不一樣種類的狀態機,適用於咱們這個案例的是有限狀態機。像它的名字同樣,有限狀態機包含有限的幾種狀態。它接收一個輸入而且基於這個輸入和當前的狀態決定下一個狀態,可能會有多種狀況輸出。當狀態機改變了狀態,咱們就稱爲它過渡到一個新的狀態。
爲了使用狀態機咱們或多或少須要定義兩件事 - 狀態和可能的過渡方法。讓咱們來嘗試實現上面提到的表單需求。
在這個表格中咱們能夠清楚的看到全部狀態和他們可能的輸出狀況。咱們一樣定義了若是輸入被傳遞進狀態機後的下一個狀態。編寫這樣的表格對你的開發週期大有裨益,由於他會回答你如下問題:
這三個問題能夠解決很是多的難題。想象一下當咱們改變內容展現的時候有一個動畫效果,當動畫開始時,UI 仍然處於以前的狀態而且用戶仍然能夠產生交互。舉個例子,用戶很是快速地點擊了兩次提交按鈕。若是不適用狀態機,咱們須要使用if語句經過標誌變量來防止代碼的執行。可是若是回到上面那個表格,咱們會看到 loading 狀態不接受 Submit 狀態的輸入。因此若是咱們在第一次點擊按鈕後把狀態機轉變爲 loading 狀態,咱們就會處於一個安全的位置。即便 Submit 輸入/動做被分發過來,狀態機也會忽略它,固然也不會再向後端發出一個請求。
狀態機模式對我來講是適用的。如下有三個理由支撐我在個人應用中使用狀態機:
如今,既然咱們知道什麼是狀態機,那就讓咱們來實現一個而且解決咱們一開始的問題。用一些嵌套的屬性定義一個簡單的對象字面量。
const machine = { currentState: 'login form', states: { 'login form': { submit: 'loading' }, 'loading': { success: 'profile', failure: 'error' }, 'profile': { viewProfile: 'profile', logout: 'login form' }, 'error': { tryAgain: 'loading' } } }
這個狀態機對象使用咱們上面表格中的內容定義了狀態。像示例中那樣,當咱們在 login form
狀態時,咱們用 submit
做爲一個輸入而且應該以 loading
狀態結束。如今咱們須要一個接收輸入的函數。
const input = function (name) { const state = machine.currentState; if (machine.states[state][name]) { machine.currentState = machine.states[state][name]; } console.log(`${ state } + ${ name } --> ${ machine.currentState }`); }
咱們得到了當前狀態而且檢查提供的input是否合法,若是經過檢查,咱們就改變當前的狀態,或者換句話說,將狀態機過渡到一個新的狀態。咱們提供了一個日誌輸出用來輸入、當前狀態和新的狀態(若是有變化的話)。下面是如何去使用咱們的狀態機:
input('tryAgain'); // login form + tryAgain --> login form input('submit'); // login form + submit --> loading input('submit'); // loading + submit --> loading input('failure'); // loading + failure --> error input('submit'); // error + submit --> error input('tryAgain'); // error + tryAgain --> loading input('success'); // loading + success --> profile input('viewProfile'); // profile + viewProfile --> profile input('logout'); // profile + logout --> login form
注意咱們嘗試經過在 login form
狀態的時候發送 tryAgain
狀態來打破狀態機的運轉或者是重複發送提交請求。在這些場景下,當前的狀態沒有被改變而且狀態機會忽略這些輸入。
我不知道狀態機的概念是否適用於你本身的場景,可是對我來講很是適用。我僅僅改變了我處理狀態管理的方式。我建議去嘗試一下,絕對是值得的。