利用 XState(有限狀態(tài)機(jī)) 編寫(xiě)易于變更的代碼
作者:jump jump
來(lái)源:SegmentFault 思否
目前來(lái)說(shuō),無(wú)論是 to c 業(yè)務(wù),還是 to b 業(yè)務(wù),對(duì)于前端開(kāi)發(fā)者的要求越來(lái)越高,各種絢麗的視覺(jué)效果,復(fù)雜的業(yè)務(wù)邏輯層出不窮。針對(duì)于業(yè)務(wù)邏輯而言,貫穿后端業(yè)務(wù)和前端交互都有一個(gè)關(guān)鍵點(diǎn) —— 狀態(tài)轉(zhuǎn)換。
當(dāng)然了,這種代碼實(shí)現(xiàn)本身并不復(fù)雜,真正的難點(diǎn)在于如何快速的進(jìn)行代碼的修改。
在實(shí)際開(kāi)發(fā)項(xiàng)目的過(guò)程中,ETC 原則,即 Easier To Change,易于變更是非常重要的。為什么解耦很好?為什么單一職責(zé)很有用?為什么好的命名很重要?因?yàn)檫@些設(shè)計(jì)原則讓你的代碼更容易發(fā)生變更。ETC 甚至可以說(shuō)是其他原則的基石,可以說(shuō),我們現(xiàn)在所作的一切都是為了更容易變更??!特別是針對(duì)于初創(chuàng)公司,更是如此。
例如:項(xiàng)目初期,當(dāng)前的網(wǎng)頁(yè)有一個(gè)模態(tài)框,可以進(jìn)行編輯,模態(tài)框上有兩個(gè)按鈕,保存與取消。這里就涉及到模態(tài)框的顯隱狀態(tài)以及權(quán)限管理。隨著時(shí)間的推移,需求和業(yè)務(wù)發(fā)生了改變。當(dāng)前列表無(wú)法展示該項(xiàng)目的所有內(nèi)容,在模態(tài)框中我們不但需要編輯數(shù)據(jù),同時(shí)需要展示數(shù)據(jù)。這時(shí)候我們還需要管理按鈕之間的聯(lián)動(dòng)。僅僅這些就較為復(fù)雜,更不用說(shuō)涉及多個(gè)業(yè)務(wù)實(shí)體以及多角色之間的細(xì)微控制。
重新審視自身代碼,雖然之前我們做了大量努力利用各種設(shè)計(jì)原則,但是想要快速而安全的修改散落到各個(gè)函數(shù)中的狀態(tài)修改,還是非常浪費(fèi)心神的,而且還很容易出現(xiàn)“漏網(wǎng)之魚(yú)”。
這時(shí)候,我們不僅僅需要依靠自身經(jīng)驗(yàn)寫(xiě)好代碼,同時(shí)也需要一些工具的輔助。
有限狀態(tài)機(jī)
有限狀態(tài)機(jī)是一個(gè)非常有用的數(shù)學(xué)計(jì)算模型,它描述了在任何給定時(shí)間只能處于一種狀態(tài)的系統(tǒng)的行為。當(dāng)然,該系統(tǒng)中只能夠建立出一些有限的、定性的“模式”或“狀態(tài)” ,并不描述與該系統(tǒng)相關(guān)的所有(可能是無(wú)限的)數(shù)據(jù)。例如,水可以是四種狀態(tài)中的一種: 固體(冰)、液體、氣體或等離子體。然而,水的溫度可以變化,它的測(cè)量是定量的和無(wú)限的。
總結(jié)來(lái)說(shuō),有限狀態(tài)機(jī)的三個(gè)特征為:
狀態(tài)總數(shù)(state)是有限的。 任一時(shí)刻,只處在一種狀態(tài)之中。 某種條件下,會(huì)從一種狀態(tài)轉(zhuǎn)變(transition)到另一種狀態(tài)。
初始狀態(tài) 觸發(fā)狀態(tài)變化的事件和轉(zhuǎn)換函數(shù) 最終狀態(tài)的集合(有可能是沒(méi)有最終狀態(tài))
const?light?=?{
??currentState:?'green',
??
??transition:?function?()?{
????switch?(this.currentState)?{
??????case?"green":
????????this.currentState?=?'yellow'
????????break;
??????case?"yellow":
????????this.currentState?=?'red'
????????break;
??????case?"red":?
????????this.currentState?=?'green'
????????break;
??????default:
????????break;
????}
??}
}
XState 體驗(yàn)
import?{?Machine?}?from?'xstate'
const?lightMachine?=?Machine({
??//?識(shí)別?id,?SCXML?id?必須唯一
??id:?'light',
??//?初始化狀態(tài),綠燈
??initial:?'green',
??
??//?狀態(tài)定義?
??states:?{
????green:?{
??????on:?{
????????//?事件名稱,如果觸發(fā)?TIMRE?事件,直接轉(zhuǎn)入?yellow?狀態(tài)
????????TIMRE:?'yellow'
??????}
????},
????yellow:?{
??????on:?{
????????TIMER:?'red'
??????}
????},
????red:?{
??????on:?{
????????TIMER:?'green'
??????}
????}
??}
})
//?設(shè)置當(dāng)前狀態(tài)
const?currentState?=?'green'
//?轉(zhuǎn)換的結(jié)果
const?nextState?=?lightMachine.transition(currentState,?'TIMER').value?
//?=>?'yellow'
//?如果傳入的事件沒(méi)有定義,則不會(huì)發(fā)生轉(zhuǎn)換,如果是嚴(yán)格模式,將會(huì)拋出錯(cuò)誤
lightMachine.transition(currentState,?'UNKNOWN').value?
跟蹤當(dāng)前狀態(tài) 執(zhí)行副作用 處理延遲過(guò)度以及時(shí)間 與外部服務(wù)溝通
import?{?Machine,interpret?}?from?'xstate'
//?。。。lightMachine 代碼
//?狀態(tài)機(jī)的實(shí)例成為?serivce
const?lightService?=?interpret(lightMachine)
???//?當(dāng)轉(zhuǎn)換時(shí)候,觸發(fā)的事件(包括初始狀態(tài))
??.onTransition(state?=>?{
????//?返回是否改變,如果狀態(tài)發(fā)生變化(或者?context?以及?action?后文提到),返回?true?
????console.log(state.changed)?
????console.log(state.value)
??})
??//?完成時(shí)候觸發(fā)
??.onDone(()?=>?{
????console.log('done')
??})
//?開(kāi)啟
lightService.start()
//?將觸發(fā)事件改為?發(fā)送消息,更適合狀態(tài)機(jī)風(fēng)格
//?初始化狀態(tài)為?green?綠色
lightService.send('TIMER')?//?yellow
lightService.send('TIMER')?//?red
//?批量活動(dòng)
lightService.send([
??'TIMER',
??'TIMER'
])
//?停止
lightService.stop()
//?從特定狀態(tài)啟動(dòng)當(dāng)前服務(wù),這對(duì)于狀態(tài)的保存以及使用更有作用
lightService.start(previousState)
import?lightMachine?from?'..'
//?react?hook?風(fēng)格
import?{?useMachine?}?from?'@xstate/react'
function?Light()?{
??const?[light,?send]?=?useMachine(lightMachine)
??
??return?<>
????//?當(dāng)前狀態(tài)?state?是否是綠色
????{light.matches('green')?&&?'綠色'}????
????//?當(dāng)前狀態(tài)的值
????{light.value}??
????//?發(fā)送消息
????
??>
}
import?{?Machine?}?from?'xstate';
const?pedestrianStates?=?{
??//?初識(shí)狀態(tài)?行走
??initial:?'walk',
??states:?{
????walk:?{
??????on:?{
????????PED_TIMER:?'wait'
??????}
????},
????wait:?{
??????on:?{
????????PED_TIMER:?'stop'
??????}
????},
????stop:?{}
??}
};
const?lightMachine?=?Machine({
??id:?'light',
??initial:?'green',
??states:?{
????green:?{
??????on:?{
????????TIMER:?'yellow'
??????}
????},
????yellow:?{
??????on:?{
????????TIMER:?'red'
??????}
????},
????red:?{
??????on:?{
????????TIMER:?'green'
??????},
??????...pedestrianStates
????}
??}
});
const?currentState?=?'yellow';
const?nextState?=?lightMachine.transition(currentState,?'TIMER').value;
//?返回級(jí)聯(lián)對(duì)象?
//?=>?{
//???red:?'walk'
//?}
//?也可以寫(xiě)為?red.walk
lightMachine.transition('red.walk',?'PED_TIMER').value;
//?轉(zhuǎn)化后返回
//?=>?{
//???red:?'wait'
//?}
//?TIMER?還可以返回下一個(gè)狀態(tài)
lightMachine.transition({?red:?'stop'?},?'TIMER').value;
//?=>?'green'
//?是否可以編輯
functions?canEdit(context:?any,?event:?any,?{?cond?}:?any)?{
??console.log(cond)
??//?=>?delay:?1000
??
??//?是否有某種權(quán)限????
??return?hasXXXAuthority(context.user)
}
const?buttonMachine?=?Machine({
??id:?'buttons',
??initial:?'green',
??//?擴(kuò)展?fàn)顟B(tài),例如?用戶等其他全局?jǐn)?shù)據(jù)
??context:?{
????//?用戶數(shù)據(jù)
????user:?{}
??},
??states:?{
????view:?{
??????on:?{
????????//?對(duì)應(yīng)之前?TIMRE:?'yellow'
????????//?實(shí)際上?字符串無(wú)法表達(dá)太多信息,需要對(duì)象表示
????????EDIT:?{
??????????target:?'edit',
??????????//?如果沒(méi)有該權(quán)限,不進(jìn)行轉(zhuǎn)換,處于原狀態(tài)
??????????//?如果沒(méi)有附加條件,直接?cond:?searchValid
??????????cond:?{
????????????type:?'searchValid',
????????????delay:?3
??????????}
????????},?
??????}
????}
??}
},?{
??//?守衛(wèi)
??guards:?{
????canEdit,
??}
})
//?XState?給予了更加合適的?API?接口,開(kāi)發(fā)時(shí)候?Context?可能不存在
//?或者我們需要在不同的上下文?context?中復(fù)用狀態(tài)機(jī),這樣代碼擴(kuò)展性更強(qiáng)
const?buttonMachineWithDelay?=?buttonMachine.withContext({
??user:?{},
??delay:?1000
})
//?withContext?是直接替換,不進(jìn)行淺層合并,但是我們可以手動(dòng)合并
const?buttonMachineWithDelay?=?buttonMachine.withContext({
??...buttonMachine.context,
??delay:?1000
})
const?timeOfDayMachine?=?Machine({
??id:?'timeOfDay',
??//?當(dāng)前不知道是什么狀態(tài)
??initial:?'unknown',
??context:?{
????time:?undefined
??},
??states:?{
????//?Transient?state
????unknown:?{
??????on:?{
????????'':?[
??????????{?target:?'morning',?cond:?'isBeforeNoon'?},
??????????{?target:?'afternoon',?cond:?'isBeforeSix'?},
??????????{?target:?'evening'?}
????????]
??????}
????},
????morning:?{},
????afternoon:?{},
????evening:?{}
??}
},?{
??guards:?{
????isBeforeNoon:?//...?確認(rèn)當(dāng)前時(shí)間是否小于?中午?
????isBeforeSix:?//?...?確認(rèn)當(dāng)前時(shí)間是否小于?下午?6?點(diǎn)
??}
});
const?timeOfDayService?=?interpret(timeOfDayMachine
??.withContext({?time:?Date.now()?}))
??.onTransition(state?=>?console.log(state.value))
??.start();
timeOfDayService.state.value?
//?根據(jù)當(dāng)前時(shí)間,可以是?morning?afternoon?和?evening,而不是?unknown?轉(zhuǎn)態(tài)
進(jìn)入和離開(kāi)某狀態(tài)觸發(fā)動(dòng)作(action 一次性)和活動(dòng)(activity 持續(xù)性觸發(fā),直到離開(kāi)某狀態(tài)) 延遲事件與過(guò)度 after 服務(wù)調(diào)用 invoke,包括 promise 以及 兩個(gè)狀態(tài)機(jī)之間相互交互 歷史狀態(tài)節(jié)點(diǎn),可以通過(guò)配置保存狀態(tài)并且回退狀態(tài)
總結(jié)
鼓勵(lì)一下
參考
XState 文檔?: https://xstate.js.org/docs/
JavaScript與有限狀態(tài)機(jī)?:?http://www.ruanyifeng.com/blog/2013/09/finite-state_machine_for_javascript.html

