摘要:集成到去使用如果想在中使用,想到比較方便的使用形式是高階組件,需要用到有限狀態機的組件傳進高階組件,就立馬擁有了使用有限狀態機的能力。
背景
近年來由于一些前端框架的興起而后逐漸成熟,組件化的概念已經深入人心,為了管理好大型應用中錯綜復雜的組件,又有了單向數據流的思想指引著我們,Vuex、Redux、MobX等狀態管理工具也許大家都信手拈來。
我們手握著這些工具,不斷思考著哪些數據應該放在全局,哪些數據應該局部消化,這樣那樣的數據該怎樣流轉。仔細想想會發現,我們一直在做的是如何將數據存在合理的地方,進而去規范怎樣使用這些數據,我們稱之為狀態管理,但我覺得好像只是做了狀態存儲與使用,卻沒有做好管理二字。嗯,總感覺差了些什么。
來看一段簡單的代碼:
state = { data: [ { id: 1, userName: xxx }, { id: 2, userName: yyy } ] }
如果根據UI = f(state)來說,上面的state.data就是一個狀態,它能直接反映視圖的樣子:
render () { const {data} = this.state return ({ data && data.length ? data.map(item =>) }{item.userName}) : "暫無數據" }
我們還會在合適的時機進行某種操作去更新狀態,比如請求獲取數據的接口就會去更新上面的data:
updateData () { getData().then(({data}) => { this.setState({data}) }) }
但隨著時間的推移,這樣的狀態會越來越多,更新狀態的方法暗藏在日益膨脹的代碼中,維護者可能自己都要如履薄冰地一翻抽絲剝繭才勉強捋清楚狀態什么時機更新,為什么要更新,更別說如果是去接盤一份祖傳代碼了。
究其原因,我覺得是沒有將狀態描述清楚,更別說管理好狀態。所以一個描述得清楚的狀態是長什么樣子呢?比如:開始的時候,data是空數組,初始化完成需要去更新data,增刪改完成后都需要更新data。
想想日常,有沒有地方能一眼就看清楚這些狀態信息?需求文檔?UI稿?靠譜嗎?
自己動手豐衣足食,我們的目標是在代碼里就能清晰地看到這些狀態信息。如果我們能夠寫一份配置文件來將它們描述清楚,然后寫代碼的時候就根據這份配置文件來寫,有修改的時候也必須先修改這份配置文件,那我們最后看配置文件就能對狀態信息一目了然了。
為了達到這樣的目標,我們得請有限狀態機來幫忙。概念性的東西請移步到JavaScript與有限狀態機,總的來說,有限狀態機是一個模型,它能描述清楚有哪些狀態,狀態之間是怎樣轉化的,它有以下特點:
1.狀態的數量是固定的
2.狀態會因為觸發了某種行為而轉變成另一種狀態(比如典型的promise,初始狀態為pending,resolve后狀態轉變成fulfilled,reject則變成rejected)
3.任意時間點狀態唯一(初始化完成了才能進行增刪改嘛)
ok,了解這些之后,我們來看看怎樣一步步達到目的。
我們以一個需求為例:
就是一個沒有一毛錢特效的Todoist,非常簡單的增刪改查。
按照之前的想法,我們首先需要一份配置文件來描述狀態:
const machine = { // 初始狀態 initial: "start", start: { INIT: "loadList" }, loadList: { LOAD_LIST_SUCCESS: "showList", LOAD_LIST_ERROR: "showListError" }, showListError: { RETRY: "loadList" }, showList: { ADD: "add", EDIT: "edit", DELETE: "delete" }, edit: { SAVE_EDIT: "saveEdit" }, saveEdit: { SAVE_EDIT_SUCCESS: "loadList" }, delete: { DELETE_SUCCESS: "loadList" }, add: { ADD_SUCCESS: "loadList" } };
配置是寫完了,現在對著上面的需求gif圖說一下這份配置是什么意思。
加載列表數據(initial: "start"表示初始狀態是start,start: {INIT: "loadList"}表示狀態start觸發INIT事件之后狀態會轉變成loadList)
加載列表數據失敗了(loadList觸發LOAD_LIST_ERROR事件狀態轉變為showListError)
加載失敗后重新加載(showListError觸發RETRY事件之后狀態重新變回loadList
重新加載列表成功(loadList觸發LOAD_LIST_SUCCESS事件狀態轉變為showList)
列表加載成功就可以對列表進行增刪改操作(showList可以觸發ADD、DELETE、EDIT事件對應增刪改操作帶來的狀態變化)
剩下的配置就不繼續寫了,可以看到通過這份配置,我們可以清晰知道,這份代碼究竟做了些什么,而且寫這份配置有利于整理好自己的思路,讓自己首先將需求過一遍,將所有邊邊角角通過寫配置預演一遍,而不是拿到需求就開擼,遇到了問題才發現之前寫的代碼不適用。同理如果需求有變,首先從這份配置入手,看看這波修改會對哪些狀態分支造成影響,就不會出現那種不知道改一個地方會不會影響到別的地方宛如拆炸彈一樣的心情。
接著,為了方便根據這份配置來進行操作,需要實現一點輔助函數:
class App extends Component { constructor(props) { state = { curState: machine.initial } } handleNextState (nextState, action) { switch (nextState) { case "loadList": // 處理loadList的邏輯 break; } } transition (action) { const { curState } = this.state; const nextState = machine[curState][action.type]; if (nextState) { this.setState({ curState: nextState }, () => this.handleNextState(nextState, action) ); } } }
基本就是這樣的結構,通過this.transition({ type: "INIT" })觸發一個事件(INIT)將當前狀態(start)轉變成另外一個狀態(loadList),而handleNextState則處理狀態轉變后的邏輯(當狀態變成loadList需要去請求接口獲取列表數據)。通過這樣的方式,我們真正將狀態管理了起來,因為我們有清晰的配置文件去描述狀態,我們有分層清晰的地方去處理當前狀態需要處理的邏輯,這就相當于有明確的戰略圖,大家都根據這份戰略圖各司其職做好自己的本分,這不是將狀態管理得井井有條嗎?
而且這樣做之后,比較容易規避一些意外的錯誤,因為任意時間點狀態唯一這個特點,帶來了狀態只能從一個狀態轉變到另一個狀態,比如點擊一個按鈕提交,這時的狀態是提交中,我們經常需要去處理用戶重復點擊而導致重復提交的事情:
let isSubmit = false const submit = () => { if (isSubmit === true) return isSubmit = true toSubmit().then(() => isSubmit = false) } submit()
使用有限狀態機進行管理后就不需要寫這種額外的isSubmit狀態,因為提交中的狀態只能轉變為提交完成。
上面的代碼完整版請點這里。
雖然初版對于狀態的管理更加清晰了一些,但仍然不夠直觀,如果能將配置轉化成圖就好了,有圖有真相嘛。心想事成:
不僅有圖可看,還可以逼真地將所有狀態都預演一遍。這個好東西就是xstate給予我們的,它是一個實現有限狀態機模型的js庫,感興趣可以去詳看,這里我們只需要按照它的寫法去寫狀態機的配置,就可以生成出這樣的圖。
看過xstate會發現,里面的東西真不少,其實如果只是想在簡單的項目上用這種模式試試水,卻要把整個庫引進來似乎不太劃算。那,不如自己來擼一個簡化版?
心動不如行動,先分析一下初版有什么不足之處。
沒有將有限狀態機的模式分離出來,如果不是用react而是用vue就用不了了。
沒有將模式分離出來導致復用性很差,總不能每個地方要用的時候都要寫一次transition等方法吧。
配置項沒寫成xstate的樣子無法使用xstate提供的工具生成圖。
現在首要的任務就是把有限狀態機的模式抽離出來,順便使用xstate的寫法來寫配置。
const is = (type, val) => Object.prototype.toString.call(val) === "[object " + type + "]"; export class Fsm { constructor(stateConfig) { // 狀態描述配置 this.stateConfig = stateConfig; // 當前狀態 this.state = stateConfig.initial; // 上一個狀態 this.lastState = ""; // 狀態離開回調集合 this.onExitMap = {}; // 狀態進入回調集合 this.onEntryMap = {}; // 狀態改變回調 this.handleStateChange = null; } /** * 改變狀態 * @param type 行為類型 描述當前狀態通過該類型的行為轉變到另一個狀態 * @param arg 轉變過程中的額外傳參 * @returns {Promise} */ transition({ type, ...arg }) { const states = this.stateConfig.states; const curState = this.state; if (!states) { throw "states undefined"; } if (!is("Object", states)) { throw "states should be object"; } if ( !states[curState] || !states[curState]["on"] || !states[curState]["on"][type] ) { console.warn(`transition fail, current state is ${this.state}`); return; } const nextState = states[curState]["on"][type]; const curStateObj = states[curState]; const nextStateObj = states[nextState]; // 狀態轉變的經歷 return ( Promise.resolve() // 狀態離開 .then(() => this.handleLifeCycle({ type: "onExit", stateObj: curStateObj, arg: { exitState: curState } }) ) // 狀態改變 .then(() => this.updateState({ state: nextState, lastState: curState })) // 進入新狀態 .then(() => this.handleLifeCycle({ type: "onEntry", stateObj: nextStateObj, arg: { state: nextState, lastState: curState, ...arg } }) ) ); } /** * 狀態改變回調 只注冊一次 * @param cb */ onStateChange(cb) { cb && is("Function", cb) && !this.handleStateChange && (this.handleStateChange = cb); } /** * 注冊狀態離開回調 * @param type * @param cb */ onExit(type, cb) { !this.onExitMap[type] && (this.onExitMap[type] = cb); } /** * 注冊狀態進入回調 * @param type * @param cb */ onEntry(type, cb) { !this.onEntryMap[type] && (this.onEntryMap[type] = cb); } /** * 更新狀態 * @param state * @param lastState */ updateState({ state, lastState }) { this.state = state; this.lastState = lastState; this.handleStateChange && this.handleStateChange({ state, lastState }); } /** * 處理狀態轉變的生命周期 * @param stateObj * @param type onExit/onEntry * @param arg * @returns {*} */ handleLifeCycle({ stateObj, type, arg }) { const cbName = stateObj[type]; if (cbName) { const cb = this[`${type}Map`][cbName]; if (cb && is("Function", cb)) { return cb(arg); } } } /** * 獲取當前狀態 * @returns {*} */ getState() { return this.state; } /** * 獲取上一個狀態 * @returns {string|*} */ getLastState() { return this.lastState; } }
然后這樣使用就好:
const stateConfig = { initial: "start", states: { start: { on: { INIT: "loadList" }, onExit: "onExitStart" }, loadList: { on: { LOAD_LIST_SUCCESS: "showList", LOAD_LIST_ERROR: "showListError" }, onEntry: "onEntryLoadList" } } } /* 結果: 1.console.log("onExitStart") 2.console.log("onEntryLoadList") 3.console.log("transition success") transition以及生命周期函數onExit、onEntry都支持promise控制異步流程 */ const fsm = new Fsm(stateConfig); transition({ type: "INIT"}).then(() => { console.log("transition success") }) fsm.onExit("onExitStart", (data) => { return new Promise(resolve => { setTimeout(() => { console.log("onExitStart") resolve() }, 1000) }) }) fsm.onEntry("onEntryLoadList", (data) => { console.log("onEntryLoadList") })
總算把有限狀態機抽成一個工具來使用了,已經完成了最關鍵的一步。
集成到react去使用如果想在react中使用,想到比較方便的使用形式是高階組件,需要用到有限狀態機的組件傳進高階組件,就立馬擁有了使用有限狀態機的能力。
import React from "react"; import { Fsm } from "../fsm"; export default function(stateConfig) { const fsm = new Fsm(stateConfig); return function(Component) { return class extends React.Component { constructor() { super(); this.state = { machineState: { // 當前狀態 value: stateConfig.initial, // 上一個狀態 lastValue: "" } }; } updateMachineState(data) { this.setState({ machineState: { ...this.state.machineState, ...data } }); } componentDidMount() { this.handleStateChange(); this.handleEvent(); } /** * 處理狀態更新 */ handleStateChange() { fsm.onStateChange(({ state, lastState }) => { this.updateMachineState({ value: state, lastValue: lastState }); }); } /** * 處理狀態改變事件 */ handleEvent() { const states = stateConfig.states; // 獲取狀態配置中所有的onEntry與onExit const eventObj = Object.keys(states).reduce( (obj, key) => { const value = states[key]; const onEntry = value.onEntry; const onExit = value.onExit; onEntry && obj.onEntry.push(onEntry); onExit && obj.onExit.push(onExit); return obj; }, { onEntry: [], onExit: [] } ); // 獲取組件實例中onEntry與onExit的回調方法 Object.keys(eventObj).forEach(key => { eventObj[key].forEach(item => { this.ref[item] && fsm[key](item, this.ref[item].bind(this.ref)); }); }); } render() { return ((this.ref = c)} {...this.state} transition={fsm.transition.bind(fsm)} /> ); } }; }; }
使用的時候就可以:
const stateConfig = { initial: "start", states: { start: { on: { INIT: "loadList" }, onExit: "onExitStart" } } } class App extends Component { componentDidMount () { this.props.transition({ type: "INIT" }); } onExitStart () { console.log("onExitStart ") } } export default withFsm(machine)(App);
現在我們可以愉快地使用這個高階組件將Todoist重構一遍。
當然,大佬們會說了,我的項目比較復雜,有沒有比較完善的解決方案呢?那肯定是有的,可以看看react-automata,將xstate集成到react中使用。由于我們上面的小高階組件用法比較像react-automata,所以基本不需要什么改動,就可以遷移到react-automata,使用react-automata再重構一遍Todoist。
對于符合有限狀態機的使用場景,使用它確實能將狀態管理起來,因為我們的狀態再也不是那種如isSubmit = false/true那樣雜亂無章的狀態,而是某個時間節點里的一個總括狀態。不管怎樣,有限狀態機的方案還是促使了我們去重新思考怎樣能更大程度地提高項目的可維護性,提供了一個新方向盡可能減少祖傳代碼,改起bug或者需求的時候分析起來更加容易,終極目的只有一個,那就是,希望能早點下班。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/97303.html
摘要:前端與狀態現在的前端開發中,對于狀態的管理是重中之重。有限狀態機那么如何更好的管理前端軟件的復雜度的狀態機思想給出了自己的答案。有限狀態機并不是一個復雜的概念簡單說,它有三個特征狀態總數是有限的。 前提 在現在的前端社區,關于MVVM、Model driven view 之類的概念,已經算是非常普及了。React/Vue 這類框架可以算是代表。而自己雖然有 React/Vue 的使用經...
摘要:相對的測試方案,市面上已經有非常多且成熟的級別的自動化測試框架,卻鮮有針對提供的自動化測試方案,原因是屬于為提供服務的插件。 作者:【友盟+】高級無線開發工程師 吳玉強、王飛 為了確保 SDK 線上運行的穩定性,我們需要在開發后進行 SDK 測試,而為了提高測試效率,而且在拓展新項目的同時能兼顧已有項目的穩定性,在有限的資源內解放測試人員到更緊急的項目中來,就需要一個自動化工具來完成工...
摘要:介一回聊狀態模式,官方描述允許一個對象在其內部狀態改變時改變它的行為。有限狀態機有限狀態機是一個非常有用的模型,可以模擬世界上大部分事物。這個是官方說法,簡單說,她有三個特征,狀態總數是有限的。,任一時刻,只處在一種狀態之中。 本回內容介紹 上一回聊了聊組合模式(Composite),用組合模式模擬了個圖片庫,聊了遞歸。介一回聊狀態模式(State),官方描述允許一個對象在其內部狀態改...
閱讀 2954·2021-11-17 09:33
閱讀 3118·2021-11-16 11:52
閱讀 482·2021-09-26 09:55
閱讀 2975·2019-08-30 15:52
閱讀 1312·2019-08-30 15:44
閱讀 1257·2019-08-30 13:59
閱讀 796·2019-08-30 13:08
閱讀 1157·2019-08-30 10:50