摘要:它們是單向數(shù)據(jù)流和狀態(tài)容器,而不是狀態(tài)管理。幾個月之前我開始尋找可以解決狀態(tài)管理問題的模式,最終我發(fā)現(xiàn)了狀態(tài)機的概念。狀態(tài)機不接受沒有明確定義的輸入作為當(dāng)前的狀態(tài)。狀態(tài)機強制開發(fā)者以聲明式的方式思考。
最近我開始思考React應(yīng)用的狀態(tài)管理。我已經(jīng)取得一些有趣的結(jié)論,并且在這篇文章里我會向你展示我們所謂的狀態(tài)管理并不是真的在管理狀態(tài)。
譯者:阿里云前端-也樹
原文鏈接:managing-state-in-javascript-with-state-machines-stent
我們避而不談的是什么(The elephant in the room)我們來看一個簡單的例子。想象這是一個展示用戶名稱、密碼和一個按鈕的表單組件。用戶會在填寫表單后點擊提交。如果一切順利,我們完成了登錄,并且有必要展示歡迎信息和一些鏈接:
我們假定這個組件有兩個展示狀態(tài)。一個是未登錄狀態(tài),另一個是用戶登錄后的狀態(tài)。所以從管理這兩種狀態(tài)開始,我們用一個布爾值的標(biāo)志位來描述用戶的狀態(tài)。
var isLoggedIn; isLoggedIn = false; // 展示表單 isLoggedIn = true; // 展示歡迎信息和鏈接
但是這樣還不夠。如果我們點擊提交按鈕后觸發(fā)的HTTP請求需要一些時間來響應(yīng),我們不能把表單孤零零的放在屏幕上,而需要更多的UI元素來展示這樣的中間狀態(tài),因此我們不得不在組件中引入另一個狀態(tài)。
現(xiàn)在我們有了第三種展示狀態(tài),僅僅用一個 isLoggedIn 變量已經(jīng)不能解決了。不走運的是我們不能設(shè)置變量值為 false-ish,它不是 true 也不是 false。當(dāng)然,我們可以引入另一個變量比如說 isInProgress。一旦我們發(fā)送請求就會把這個變量的值置為 true。這個變量會告訴我們是處于請求的過程中并且用戶應(yīng)該看到加載中的展示狀態(tài)。
var isLoggedIn; var isInProgress; // 展示表單 isLoggedIn = false; isInProgress = false; // 請求過程中 isLoggedIn = false; isInProgress = true; // 展示歡迎信息和鏈接 isLoggedIn = true; isInProgress = false;
非常棒!我們用到兩個變量并且需要記住這三種情況對應(yīng)的變量值。看起來我們解決了問題。但另外的問題是,我們維護了太多狀態(tài)。如果我們需要展示一個請求成功的信息,或者一切順利的時候我們需要告知用戶:“Yep, 你成功登錄了”,并且兩秒后信息伴隨著華麗的動畫隱藏起來,接著展示出最終的界面,要怎么辦?
現(xiàn)在情況變得有些復(fù)雜。我們有了 isLoggedIn 和 isInProgress,但是看起來僅僅使用它們還不夠。isInProgress 在請求結(jié)束后確實是 false,但是他的默認(rèn)值同樣是 false。我覺得我們需要第三個變量 - isSuccessful。
var isLoggedIn, isInProgress, isSuccessful; // 展示表單 isLoggedIn = false; isInProgress = false; isSuccessful = false; // 請求過程中 isLoggedIn = false; isInProgress = true; isSuccessful = false; // 展示成功狀態(tài) isLoggedIn = true; isInProgress = false; isSuccessful = true; // 展示歡迎信息和鏈接 isLoggedIn = true; isInProgress = false; isSuccessful = false;
我們簡單的狀態(tài)管理一步步變成了由 if-else 組成的巨大的條件網(wǎng),很難去理解和維護。
if (isInProgress) { // 請求過程中 } else if (isLoggedIn) { if (isSuccessful) { // 展示請求成功信息 } else { // 展示歡迎信息和鏈接 } } else { // 等待輸入,展示表單 }
我們還有一個問題會讓這個情景變得更糟:如果請求失敗我們要怎么做?我們需要展示一個錯誤信息和一個重試鏈接,如果點擊重試我們會重復(fù)一次請求的過程。
現(xiàn)在我們的代碼已經(jīng)沒有任何可維護性。我們有非常多的場景需要滿足,僅僅依賴引入新的變量是不可接受的。讓我們想想是否可以通過更好的命名方式來解決,同時可能還需要引入一個新的條件聲明。
isInProgress 僅僅在請求的過程中被用到。我們現(xiàn)在還關(guān)心請求結(jié)束之后的過程。
isLoggedIn 有一點誤導(dǎo)的含義,因為我們只要請求結(jié)束就把它置為 true。而如果請求出錯,用戶并沒有真正登入。所以我們把它重命名為 isRequestFinished。雖然看起來好些了,但是它僅僅代表我們從服務(wù)器獲得了響應(yīng),并不能用它來判斷響應(yīng)是否為錯誤。
isSuccessful 是一個最終狀態(tài)合適的候選變量。如果請求出錯我們可以把它設(shè)置為 false,但是等等,它的默認(rèn)值也是 false。所以它也不能作為代表錯誤狀態(tài)的變量。
我們需要第四個變量,isFailed 怎么樣?
var isRequestFinished, isInProgress, isSuccessful, isFailed; if (isInProgress) { // 請求過程中 } else if (isRequestFinished) { if (isSuccessful) { // 展示請求成功信息 } else if (isFailed) { // 展示請求失敗信息和重試鏈接 } else { // 展示歡迎信息和鏈接 } } else { // 等待輸入,展示表單 }
這四個變量描述了一個看似簡單但實際并不簡單的過程,這個過程包含了許多邊界情況。當(dāng)項目進一步迭代時,最終可能會由于已有變量的組合不能滿足新的需求,而定義更多的變量。這就是構(gòu)建用戶界面十分困難的原因。
我們需要更好的狀態(tài)管理方式。也許可以使用更現(xiàn)代和更流行的概念。
Flux 或者 Redux 怎么樣?最近我在思考 Flux 架構(gòu)和 Redux 庫在狀態(tài)管理中的定位。即使這些工具和狀態(tài)管理有關(guān),但是它們本質(zhì)上不是解決這類問題的。
Flux 是 Facebook 用來構(gòu)建客戶端 web 應(yīng)用的架構(gòu)。它利用單向數(shù)據(jù)流補足了 React 的視圖組件的組織方式。
Redux 是一個可預(yù)測的狀態(tài)容器,用來構(gòu)建 JavaScript 應(yīng)用。
它們是 “單向數(shù)據(jù)流” 和 “狀態(tài)容器”,而不是 “狀態(tài)管理”。Flux 和 Redux 背后的概念是非常實用和討巧的。我認(rèn)為它們是適合構(gòu)建用戶界面的方式。單向數(shù)據(jù)流讓數(shù)據(jù)擁有可預(yù)測性,改進了前端開發(fā)。Redux 中的 reducer 擁有的不可變特性,提供了一種可以減少 bug 的數(shù)據(jù)傳送方式。
就我的感受來說,這些模式更適用于數(shù)據(jù)管理和數(shù)據(jù)流管理。它們提供了完善的 API 來交換改變我們應(yīng)用數(shù)據(jù)的信息,但是并不能解決我們狀態(tài)管理的問題。這也因為這些問題是跟項目強相關(guān)的,問題的上下文取決于我們正在做的事情。
當(dāng)然像處理 HTTP 請求我們可以通過某個庫來解決,但是對其它相關(guān)的業(yè)務(wù)邏輯我們?nèi)匀恍枰约壕帉懘a來實現(xiàn)。問題在于我們?nèi)绾斡靡环N合適的方式去組織這些代碼,而不至于每兩年就把整個應(yīng)用重寫一遍。
幾個月之前我開始尋找可以解決狀態(tài)管理問題的模式,最終我發(fā)現(xiàn)了狀態(tài)機的概念。事實上我們一直都在構(gòu)建狀態(tài)機,只不過我們不知道。
什么是狀態(tài)機?狀態(tài)機的數(shù)學(xué)定義是一個計算模型,我的理解是:狀態(tài)機就是保存你的狀態(tài)和狀態(tài)變化的一個盒子。這里有一些不同種類的狀態(tài)機,適用于我們這個案例的是有限狀態(tài)機。像它的名字一樣,有限狀態(tài)機包含有限的幾種狀態(tài)。它接收一個輸入并且基于這個輸入和當(dāng)前的狀態(tài)決定下一個狀態(tài),可能會有多種情況輸出。當(dāng)狀態(tài)機改變了狀態(tài),我們就稱為它過渡到一個新的狀態(tài)。
實戰(zhàn)狀態(tài)機為了使用狀態(tài)機我們或多或少需要定義兩件事 - 狀態(tài)和可能的過渡方法。讓我們來嘗試實現(xiàn)上面提到的表單需求。
在這個表格中我們可以清楚的看到所有狀態(tài)和他們可能的輸出情況。我們同樣定義了如果輸入被傳遞進狀態(tài)機后的下一個狀態(tài)。編寫這樣的表格對你的開發(fā)周期大有裨益,因為他會回答你以下問題:
用戶界面可能出現(xiàn)的所有狀態(tài)有哪些?
每種狀態(tài)之間會發(fā)生什么?
如果某種狀態(tài)改變,結(jié)果是什么?
這三個問題可以解決非常多的難題。想象一下當(dāng)我們改變內(nèi)容展示的時候有一個動畫效果,當(dāng)動畫開始時,UI 仍然處于之前的狀態(tài)并且用戶仍然可以產(chǎn)生交互。舉個例子,用戶非常快速地點擊了兩次提交按鈕。如果不適用狀態(tài)機,我們需要使用if語句通過標(biāo)志變量來防止代碼的執(zhí)行。但是如果回到上面那個表格,我們會看到 loading 狀態(tài)不接受 Submit 狀態(tài)的輸入。所以如果我們在第一次點擊按鈕后把狀態(tài)機轉(zhuǎn)變?yōu)?loading 狀態(tài),我們就會處于一個安全的位置。即使 Submit 輸入/動作被分發(fā)過來,狀態(tài)機也會忽略它,當(dāng)然也不會再向后端發(fā)出一個請求。
狀態(tài)機模式對我來說是適用的。以下有三個理由支撐我在我的應(yīng)用中使用狀態(tài)機:
狀態(tài)機模式免去了很多可能出現(xiàn)的 bug 和奇怪的清潔,因為它不會讓 UI 變化為我們不知道的狀態(tài)。
狀態(tài)機不接受沒有明確定義的輸入作為當(dāng)前的狀態(tài)。這會免去我們對其它代碼執(zhí)行的部分容錯處理。
狀態(tài)機強制開發(fā)者以聲明式的方式思考。因為我們大部分的邏輯需要提前定義。
在 JavaScript 里實現(xiàn)狀態(tài)機現(xiàn)在,既然我們知道什么是狀態(tài)機,那就讓我們來實現(xiàn)一個并且解決我們一開始的問題。用一些嵌套的屬性定義一個簡單的對象字面量。
const machine = { currentState: "login form", states: { "login form": { submit: "loading" }, "loading": { success: "profile", failure: "error" }, "profile": { viewProfile: "profile", logout: "login form" }, "error": { tryAgain: "loading" } } }
這個狀態(tài)機對象使用我們上面表格中的內(nèi)容定義了狀態(tài)。像示例中那樣,當(dāng)我們在 login form 狀態(tài)時,我們用 submit 作為一個輸入并且應(yīng)該以 loading 狀態(tài)結(jié)束。現(xiàn)在我們需要一個接收輸入的函數(shù)。
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 }`); }
我們獲得了當(dāng)前狀態(tài)并且檢查提供的input是否合法,如果通過檢查,我們就改變當(dāng)前的狀態(tài),或者換句話說,將狀態(tài)機過渡到一個新的狀態(tài)。我們提供了一個日志輸出用來輸入、當(dāng)前狀態(tài)和新的狀態(tài)(如果有變化的話)。下面是如何去使用我們的狀態(tài)機:
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 狀態(tài)的時候發(fā)送 tryAgain 狀態(tài)來打破狀態(tài)機的運轉(zhuǎn)或者是重復(fù)發(fā)送提交請求。在這些場景下,當(dāng)前的狀態(tài)沒有被改變并且狀態(tài)機會忽略這些輸入。
最后的話我不知道狀態(tài)機的概念是否適用于你自己的場景,但是對我來說非常適用。我僅僅改變了我處理狀態(tài)管理的方式。我建議去嘗試一下,絕對是值得的。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/89524.html
摘要:發(fā)布按照官方發(fā)布計劃,的發(fā)布意味著進入階段,徹底退出舞臺,的還有半年結(jié)束。為了應(yīng)對這個挑戰(zhàn),美團點評境外度假前端研發(fā)團隊自年月起啟動了面向端用戶的赫爾墨斯項目。前端技術(shù)越來越復(fù)雜,有不低的技術(shù)門檻。 推薦 1. 利用 Dawn 工程化工具實踐 MobX 數(shù)據(jù)流管理方案 https://zhuanlan.zhihu.com/p/... 項目在最初應(yīng)用 MobX 時,對較為復(fù)雜的多人協(xié)作項...
摘要:前言前端模塊化,主要是解決兩個問題命名空間沖突,文件依賴管理。目前解決的方法是模塊化命名空間各個模塊的命名空間獨立。模塊化構(gòu)建工具,等是用來組織前端模塊的構(gòu)建工具加載器。 前言 前端模塊化,主要是解決兩個問題——命名空間沖突,文件依賴管理。 坑___命名空間沖突 我自己測試好的代碼和大家合并后怎么起沖突了? 頁面腳本的變量或函數(shù)覆蓋了公有腳本的。 坑___文件依賴管理 明明項目需...
摘要:三思而后行自動化測試最終目的是啥投入產(chǎn)出比的最佳平衡點在哪很多實施者在搭建自動化框架前往往缺乏思考,為了自動化而自動化。 三思而后行 UI自動化測試最終目的是啥?投入產(chǎn)出比的最佳平衡點在哪?很多實施者在搭建UI自動化框架前往往缺乏思考,為了自動化而自動化。三思而后行,方向決定成敗。由于項目接口(API and Service)自動化代碼行覆蓋率已經(jīng)達到70%,基于當(dāng)前自動化人力和項目質(zhì)...
摘要:受上海杰克大大委托,于今晚分享一下本人的自學(xué)歷程主題機械轉(zhuǎn)行前端,半年零基礎(chǔ)自學(xué)的心路歷程。所以我就這半年個人自學(xué)修行以來的一些感觸和心得方面進行分享。背景介紹內(nèi)容前工作狀況機械離職經(jīng)歷心態(tài)轉(zhuǎn)變目標(biāo)確定大家好,我是,一枚前端萌新。 機械轉(zhuǎn)行前端,半年零基礎(chǔ)自學(xué)的心路歷程 標(biāo)簽: 轉(zhuǎn)行 自學(xué) 原創(chuàng):Michael.Lu 277133779@qq .com 轉(zhuǎn)載注明出處 這是初級群(西安...
閱讀 3506·2023-04-26 02:44
閱讀 1637·2021-11-25 09:43
閱讀 1531·2021-11-08 13:27
閱讀 1895·2021-09-09 09:33
閱讀 910·2019-08-30 15:53
閱讀 1773·2019-08-30 15:53
閱讀 2783·2019-08-30 15:53
閱讀 3117·2019-08-30 15:44