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