做網(wǎng)站每頁面費(fèi)用/bing搜索引擎國(guó)際版
有限狀態(tài)機(jī)
是一種數(shù)學(xué)計(jì)算模型,它描述了在任何給定時(shí)間只能處于一種狀態(tài)的系統(tǒng)的行為。形式上,有限狀態(tài)機(jī)有五個(gè)部分:
- 初始狀態(tài)值 (
initial state
) - 有限的一組狀態(tài) (
states
) - 有限的一組事件 (
events
) - 由事件驅(qū)動(dòng)的一組狀態(tài)轉(zhuǎn)移關(guān)系 (
transitions
) - 有限的一組最終狀態(tài) (
final states
)
狀態(tài)是指由狀態(tài)機(jī)建模的系統(tǒng)中某種有限的
、定性的
“模式”或“狀態(tài)”,并不描述與該系統(tǒng)相關(guān)的所有(可能是無限的)數(shù)據(jù)。例如,水可以處于以下 4 種狀態(tài)中的一種:冰、液體、氣體或等離子體。然而,水的溫度可以變化,所以其測(cè)量值是定量的和無限的。再比如管理TCP Socket
連接時(shí),其生命周期內(nèi)存在明顯的有限狀態(tài)轉(zhuǎn)換。
可能有相當(dāng)多的同學(xué)在開發(fā)中沒意識(shí)到有限狀態(tài)機(jī)
的作用,但是實(shí)際上,我們幾乎無時(shí)不刻在有意無意間使用了有限狀態(tài)機(jī)。當(dāng)您在開發(fā)過程中能有意識(shí)地系統(tǒng)地進(jìn)行有限狀態(tài)分析并應(yīng)用有限狀態(tài)機(jī),往往代表著您達(dá)到了較高的水平。
目前開源的有限狀態(tài)機(jī)實(shí)現(xiàn)中比較知名的有:
xstate
:堪稱狀態(tài)機(jī)航空母艦,功能太強(qiáng)大了,也太復(fù)雜了,學(xué)習(xí)成本非常高。Javascript State Machine
:功能較弱,在實(shí)際試用過程中發(fā)現(xiàn)在進(jìn)行異步切換時(shí)存在問題。jssm
:特點(diǎn)是引入自己的DSL語法來描述狀態(tài)機(jī),使用起來比較別扭。
事實(shí)上,從功能完整度上看xstate
是第一選擇,但是其過于復(fù)雜了,在功能與易用平衡方面并不理想。
因此,我們開發(fā)了FlexState
有限狀態(tài)機(jī),力求在功能性、易用性上達(dá)到平衡。
FlexState
是一款簡(jiǎn)單易用的有限狀態(tài)機(jī),具有以下特性:
- 支持基于
Class
構(gòu)建有限狀態(tài)機(jī)實(shí)例 - 支持狀態(tài)
enter/leave/resume/done
鉤子事件 - 狀態(tài)切換完全支持異步操作
- 支持定義異步狀態(tài)動(dòng)作
Action
- 支持狀態(tài)切換生命周期事件訂閱
- 支持錯(cuò)誤處理和狀態(tài)切換中止
- 基于
TypeScript
開發(fā) - 支持子狀態(tài)
- 核心代碼
90%+
單元測(cè)試覆蓋率
Github
官網(wǎng)
快速入門
下面我們以開發(fā)基于nodejs/net.socket
的TCP客戶端為例來說明FlexStateMachine
的使用。
作為例子,我們?yōu)?code>TCPClient設(shè)計(jì)以下幾種狀態(tài):
Initial
:初始狀態(tài),構(gòu)建socket實(shí)例后處于該階段。Connecting
:連接中,當(dāng)調(diào)用Connect方法,觸發(fā)connect事件前。Connected
:已連接,當(dāng)觸發(fā)connect事件后。Disconnecting
:正在斷開,當(dāng)調(diào)用destory或end方法后,end/close事件觸發(fā)前。Disconnected
:被動(dòng)斷開,當(dāng)觸發(fā)end/close事件后。AlwaysDisconnected
: 主動(dòng)斷開狀態(tài)IDLE
: 自動(dòng)添加的空閑狀態(tài),狀態(tài)機(jī)未啟動(dòng)時(shí)ERROR
: 自動(dòng)添加的錯(cuò)誤狀態(tài),特殊的FINAL
狀態(tài)
TCPClient
的狀態(tài)圖如下:
第一步:構(gòu)建狀態(tài)機(jī)
推薦直接繼承FlexStateMachine
來創(chuàng)建一個(gè)TCPClient
實(shí)例,該種方式更加簡(jiǎn)單易用。
import { state, FlexStateMachine } from "flexstate"class TcpClient extends FlexStateMachine{// 定義狀態(tài)static states = { Initial : { value:0, title:"已初始化", next:["Connecting","Connected","Disconnected"],initial:true},Connecting : { value:1, title:"正在連接...", next:["Connected","Disconnected"] },Connected : { value:2, title:"已連接", next:["Disconnecting","Disconnected"] },Disconnecting : { value:3, title:"正在斷開連接...", next:["Disconnected"] },Disconnected : {value:4, title:"已斷開連接", next:["Connecting"]},AlwaysDisconnected : {value:5, title:"已主動(dòng)斷開連接", next:["Connecting"]}} constructor(options:FlexStateOptions){super(Object.assign({host:"",port:9000,autoStart:true,context : null, // 狀態(tài)上下文對(duì)象,當(dāng)執(zhí)行動(dòng)作或狀態(tài)轉(zhuǎn)換事件時(shí)的this指向autoStart : true, // 自動(dòng)啟動(dòng)狀態(tài)機(jī)timeout : 30 * 1000 // 當(dāng)執(zhí)行狀態(tài)切換回調(diào)時(shí)的超時(shí),如enter、leave、done回調(diào)injectActionMethod: true, // 將動(dòng)作方法注入到當(dāng)前實(shí)例中 },options)) } @state{when:["Initial","Disconnected","Error"], // 代表只能當(dāng)處于此三種狀態(tài)時(shí)才允許調(diào)用連接方法 pending:"Connecting", // 執(zhí)行后進(jìn)入正在連接中的狀態(tài)}connect(){this._socket.connect(this.options) } @state({when:["Connected"], // 代表只有在已連接狀態(tài)才允許執(zhí)行斷開方法pending:"Disconnecting"}) disconnect(){this._socket.destory()}// 當(dāng)狀態(tài)轉(zhuǎn)換成功后會(huì)調(diào)用此方法ontTransition({error,from,to,done,timeConsuming}){console.log(`從<${previous}>轉(zhuǎn)換到<current>,耗時(shí):${timeConsuming}ms`) // 例 ==> 從<Connecting>轉(zhuǎn)換到<Connected,耗時(shí)12ms>console.log(this.current) // {name,value,....}}onData(data){....}}
說明:
- 以上我們創(chuàng)建了一個(gè)繼承自
FlexStateMachine
來創(chuàng)建一個(gè)TCPClient
實(shí)例 - 并且定義了
Initial
、Connecting
、Connected
、Disconnecting
、Disconnected
、AlwaysDisconnected
共六個(gè)狀態(tài)以及狀態(tài)之間的轉(zhuǎn)換約束。同時(shí),狀態(tài)機(jī)還會(huì)自動(dòng)添加一個(gè)ERROR
和IDLE
狀態(tài)。 - 定義了
connect
和disconnect
兩個(gè)動(dòng)作action
,在這兩個(gè)方法前添加@state
代表了當(dāng)執(zhí)行這兩個(gè)方法會(huì)導(dǎo)致狀態(tài)變化。
第二步:初始化TCPSocket
當(dāng)實(shí)例化TCPClient
實(shí)例后,首先應(yīng)該創(chuàng)建Socket實(shí)例。由于TCPClient
實(shí)例繼承自FlexStateMachine
,并且我們指定了Initial
為初始化狀態(tài)。
狀態(tài)機(jī)會(huì)在實(shí)例化并啟動(dòng)后自動(dòng)轉(zhuǎn)換到Initial
狀態(tài)。因此,我們可以在進(jìn)入Initial
狀態(tài)前進(jìn)行初始化操作。
class TcpClient extends FlexStateMachine{// 轉(zhuǎn)換至Initial狀態(tài)前會(huì)調(diào)用方法async onInitialEnter({retry,retryCount}){try{ this._socket = new net.Socket()// 當(dāng)連接成功時(shí),切換到Connected事件; 每一個(gè)狀態(tài)均有一個(gè)大寫的狀態(tài)值實(shí)例成員// this.CONNECTED==this.states.Connected.valuethis._socket.on("connect",()=>this.transition(this.CONNECTED)) this._socket.on("close",()=>{//.... 詳見后續(xù)重連說明}) // 套接字因不活動(dòng)而超時(shí)則觸發(fā),這只是通知套接字已空閑,用戶必須手動(dòng)關(guān)閉連接。// 通過事件觸發(fā)方式來執(zhí)行disconnect動(dòng)作this._socket.on("timeout",()=>this.emit("disconnect"))this._socket.on("error",()=>this.transition(this.ERROR))this._socket.on("data",this.onData.bind(this))}catch(e){if(retryCount<3){retry(1000) // 1000ms后重試執(zhí)行}else{ //throw e} }
}
當(dāng)TCPClient
實(shí)例化,狀態(tài)機(jī)處于IDLE
狀態(tài)(<tcp實(shí)例>.current.name=='IDLE'
),然后狀態(tài)機(jī)自動(dòng)啟動(dòng)(autoStart=true
)將轉(zhuǎn)換至Initial
狀態(tài)(initial
狀態(tài))。
- 狀態(tài)機(jī)轉(zhuǎn)換至
Initial
狀態(tài)前會(huì)調(diào)用onInitialEnter
。我們可以在此方法中創(chuàng)建TCP Socket
實(shí)例以及其他相關(guān)的初始化。 onInitialEnter
成功執(zhí)行完畢后,狀態(tài)機(jī)的狀態(tài)將轉(zhuǎn)換至Initial
。(IDLE
->Initial
)- 如果在
onInitialEnter
函數(shù)初始化失敗或出錯(cuò),則應(yīng)該拋出錯(cuò)誤。錯(cuò)誤將導(dǎo)致狀態(tài)機(jī)將無法轉(zhuǎn)換至Initial
狀態(tài),也就無法進(jìn)行后續(xù)的所有操作了。一般在初始化失敗時(shí),會(huì)進(jìn)行如下操作:- 進(jìn)行重試操作,直至初始化成功(即成功創(chuàng)建好Socket并進(jìn)行相應(yīng)的事件綁定)。
- 反復(fù)重試多次失敗后,也可能會(huì)放棄重試,
TCP Client
將無法切換到Initial
狀態(tài),而是保持在IDLE
狀態(tài)。 - 當(dāng)條件具備時(shí),狀態(tài)機(jī)需要重新運(yùn)行(即調(diào)用
tcp.start()
來啟動(dòng)狀態(tài)機(jī)),將重復(fù)上述過程。
第三步:連接服務(wù)器
當(dāng)TCPClient
實(shí)例初始化完成后,就可以開始連接服務(wù)器。我們可以在類上創(chuàng)建狀態(tài)機(jī)動(dòng)作connect
,啟動(dòng)連接操作。
import { state, FlexStateMachine } from "flexstate"class TcpClient extends FlexStateMachine{// 通過裝飾器來聲明這是一個(gè)狀態(tài)動(dòng)作 @state({// 代表只能當(dāng)處于此三種狀態(tài)時(shí)才允許執(zhí)行動(dòng)作,即調(diào)用連接方法when:["Initial","Disconnected","Error"], // 執(zhí)行后進(jìn)入正在連接中的狀態(tài)pending:"Connecting" }) async connect(){ this._socket.connect(this.options) }
}
// 創(chuàng)建連接實(shí)例
let tcp = new TcpClient({...})
// 連接
tcp.connect()
// 狀態(tài)機(jī)狀態(tài)將變化: Initial -> Connecting -> Connected
// 如果連接出錯(cuò)狀態(tài)將變化:Initial -> Connecting -> Error
上述的@state({....})
定義了一個(gè)狀態(tài)機(jī)動(dòng)作,代表當(dāng)調(diào)用connect
方法時(shí)會(huì)導(dǎo)致一系列的狀態(tài)轉(zhuǎn)換:
- 動(dòng)作名稱為
connect
,會(huì)創(chuàng)建一個(gè)同名的實(shí)例方法tcp.connect
替換掉原始的connect
方法。 when
參數(shù)代表了只有當(dāng)前狀態(tài)為[Initial
、Disconnected
、Error
]其中一個(gè)時(shí)才允許執(zhí)行connect
動(dòng)作。pending="Connecting"
代表,執(zhí)行connect
動(dòng)作前,狀態(tài)機(jī)的狀態(tài)將暫時(shí)會(huì)切換至Connecting
,也就是會(huì)顯示正在連接中
。由于連接操作可能是耗時(shí)的,所有設(shè)計(jì)一個(gè)正在連接中是比較符合實(shí)際業(yè)務(wù)邏輯的。- 如果執(zhí)行
socket.connect({...})
出錯(cuò),可以通過@state({retry,retryCount})
來啟用重試邏輯。需要注意的是 調(diào)用connect
成功僅僅代表該方法在調(diào)用時(shí)沒有出錯(cuò),并不代表已經(jīng)連接成功。是否連接成功需要由socket/connect
事件來觸發(fā)確認(rèn)。 - 在上述中,并沒有顯式指定當(dāng)連接成功時(shí)的狀態(tài),原因是因?yàn)?code>connect方法是一個(gè)異步方法,是否連接成功或失敗是通過事件回調(diào)的方式轉(zhuǎn)換狀態(tài)的。在初始化階段,我們訂閱了
close
、end
等回調(diào)。this._socket.on("close",()=>this.transition(this.DISCONNECTED))
this._socket.on("end",()=>this.transition(this.DISCONNECTED))
this._socket.on("error",()=>this.transition(this.ERROR))
當(dāng)執(zhí)行socket.connect
方法后,如果接收到close
/end
/error
則會(huì)轉(zhuǎn)換到對(duì)應(yīng)的DISCONNECTED
、ERROR
狀態(tài)。
- 至此,實(shí)現(xiàn)了當(dāng)
tcp.connect
方法,狀態(tài)轉(zhuǎn)換到Connecting
狀態(tài),連接成功轉(zhuǎn)換至Connected
狀態(tài),連接被斷開轉(zhuǎn)換至Disconnected
狀態(tài),出現(xiàn)錯(cuò)誤時(shí)轉(zhuǎn)換到ERROR
狀態(tài)。并且在出錯(cuò)時(shí)會(huì)進(jìn)行一定重試操作,更多關(guān)于重試的內(nèi)容詳見后續(xù)介紹。
第四步:偵聽連接狀態(tài)
在TCP連接生命周期內(nèi),狀態(tài)機(jī)會(huì)在最后Initial/Connecting/Connected/Disconnecting/Disconnected/AlwaysDisconnected
狀態(tài)之間進(jìn)行轉(zhuǎn)換,我們希望可能偵聽狀態(tài)機(jī)的狀態(tài)轉(zhuǎn)換事件,以便在連接發(fā)生狀態(tài)轉(zhuǎn)換時(shí)進(jìn)行一些操作,此時(shí)就可以偵聽各種連接事件。
偵聽連接狀態(tài)有兩種方法:
FlexStateMachine
本身就是一個(gè)EventEmitter,可以通過訂閱事方式進(jìn)行偵聽。
// *****偵聽某個(gè)狀態(tài)事件*****tcp.on("Connected/enter",({from,to})=>{// 當(dāng)準(zhǔn)備進(jìn)入連接前狀態(tài)時(shí)觸發(fā)此事件
}) tcp.on("Connected/leave",({from,to})=>{// 當(dāng)準(zhǔn)備要離開連接狀態(tài)時(shí)觸發(fā)此事件
}) tcp.on("Connected/done",({from,to})=>{// 當(dāng)切換至連接狀態(tài)后觸發(fā)此事件
})
- 在類中也可以直接定義
on<狀態(tài)名>Enter
、on<狀態(tài)名>
、on<狀態(tài)名>Done
、on<狀態(tài)名>Leave
類方法來偵聽事件。
class TcpClient extends FlexStateMachine{onInitialEnter({from,to}){...} // 進(jìn)入Initial狀態(tài)前onInitial({from,to}){...} // 已切換至Initial狀態(tài)onInitialDone({from,to}){...} // ===onInitialonInitialLeave({from,to}){...} // 離開Initial狀態(tài)時(shí)onConnectingEnter({from,to}){...} // 進(jìn)入Connecting狀態(tài)前onConnecting({from,to}){...} // 已切換至Connecting狀態(tài)onConnectingDone({from,to}){...} // === onConnectingonConnectingLeave({from,to}){...} // 離開Connecting狀態(tài)時(shí)onConnectedEnter({from,to}){...} // 進(jìn)入Connected狀態(tài)前onConnected({from,to}){...} // 已切換至Connected狀態(tài)onConnectedDone({from,to}){...} // ===onConnectedonConnectedLeave({from,to}){...} // 離開Connected狀態(tài)時(shí)//...所有狀態(tài)均可以定義on<狀態(tài)名>Enter、on<狀態(tài)名>、on<狀態(tài)名>Leave事件 }
第五步:斷開重新連接
連接管理中的斷開重連是非常重要的功能,要處理此邏輯,首先分析一下什么情況下會(huì)斷開連接。
斷開連接一般包括主動(dòng)
和被動(dòng)
兩種情況:
- 服務(wù)器或網(wǎng)絡(luò)問題等導(dǎo)致的連接斷開
此種情況屬于客戶端被動(dòng)斷開連接,一般會(huì)需要進(jìn)行自動(dòng)重新連接。服務(wù)器主動(dòng)斷開時(shí),客戶端會(huì)偵聽到end
事件,直接進(jìn)入斷開狀態(tài)。即狀態(tài)機(jī)不會(huì)切換到Disconnecting
,而是直接至Disconnected
。
- 客戶端主動(dòng)斷開連接
此種情況屬性客戶主動(dòng)斷開連接發(fā),就是客戶端主動(dòng)調(diào)用disconnect
方法,一般是不需要進(jìn)行自動(dòng)重連的。
主動(dòng)斷開時(shí),需要調(diào)用socket.end
方法,然后等待end
事件的觸發(fā)。狀態(tài)機(jī)會(huì)經(jīng)歷從Disconnecting
到Disconnected
的過程。
無論是主動(dòng)斷開連接還是被動(dòng)斷開連接,均會(huì)觸發(fā)close
事件,因此需要在close
事件觸發(fā)時(shí)區(qū)別是主動(dòng)斷開還是被動(dòng)斷開。
為了更好地區(qū)別主動(dòng)斷開
和被動(dòng)斷開
,我們可以增加一個(gè)狀態(tài)AlwaysDisconnected
來代表是客戶端主動(dòng)斷開,AlwaysDisconnected
被設(shè)計(jì)為FINAL
狀態(tài)。
當(dāng)狀態(tài)機(jī)切換到Disconnected
狀態(tài)時(shí)調(diào)用connect
動(dòng)作方法來重新連接。當(dāng)狀態(tài)機(jī)切換到AlwaysDisconnected
時(shí),則不進(jìn)行重新連接。
兩者差別在于,如果是主動(dòng)斷開會(huì)經(jīng)歷Disconnecting
狀態(tài),而被動(dòng)斷開則不會(huì)經(jīng)過此狀態(tài),因此我們就可以在on("close")
事件中處理將狀態(tài)轉(zhuǎn)換至AlwaysDisconnected
或DISCONNECTED
。
class TcpClient extends FlexStateMachine{class TcpClient extends FlexStateMachine{...// 轉(zhuǎn)換至Initial狀態(tài)前會(huì)調(diào)用方法async onInitialEnter({retry,retryCount}){// 在此需要確認(rèn)該切換到Disconnected還是AlwaysDisconnected狀態(tài)this._socket.on("close",()=>{// 主動(dòng)調(diào)用disconnect方法時(shí),狀態(tài)機(jī)才會(huì)切換到Disconnectingif(this.current.name==="Disconnecting"){ this.transition(this.ALWAYSDISCONNECTED)}else{this.transition(this.DISCONNECTED)}})}// 當(dāng)切換至Disconnected狀態(tài)的回調(diào)async onDisconnected({from,to}){await delay(3000)this.connect() // 重新執(zhí)行Connect動(dòng)作}//async onConnectClosed({from,to}){}@state({when:"Connected",pending:"Disconnecting"// 由于調(diào)用end方法是異步操作,需要等待close事件觸發(fā)后,才是真正的斷開連接 // 因此,不能在調(diào)用disconnected返回后就將狀態(tài)設(shè)置為AlwaysDisconnected// 也就是說不要在此配置rejected參數(shù);// 假設(shè)執(zhí)行this._socket.end沒有出錯(cuò),則狀態(tài)將保持在Disconnecting狀態(tài),直至this._socket.on("close",callback)時(shí)才進(jìn)行狀態(tài)轉(zhuǎn)換// rejected:"" })async disconnect(){// 注意:此操作是異步狀態(tài)this._socket.end() }
}
第六步:連接認(rèn)證子狀態(tài)
當(dāng)tcp連接成功后,一般服務(wù)器會(huì)要求對(duì)客戶連接進(jìn)行認(rèn)證才允許進(jìn)行使用,而認(rèn)證操作(login/logout
)是一個(gè)耗時(shí)的異步操作,同樣需要進(jìn)行狀態(tài)管理。當(dāng)進(jìn)入Connected
狀態(tài)后,狀態(tài)將在未認(rèn)證
、正在認(rèn)證
、已認(rèn)證
三個(gè)狀態(tài)間進(jìn)行轉(zhuǎn)換,并且在連接斷開或者出錯(cuò)時(shí)馬上退出這三個(gè)狀態(tài)。因此,就有必要引入子狀態(tài)的概念。
引入子狀態(tài)后,對(duì)應(yīng)的狀態(tài)圖更新如下:
class TcpClient extends FlexStateMachine{static states = { Connected : { value:2, title:"已連接", next:["Disconnecting","Disconnected","Error"] // 定義一個(gè)獨(dú)立的狀態(tài)機(jī)域scope:{states:{Unauthenticated : {value:0,title:"未認(rèn)證",initial:true,next:["Authenticating"]},Authenticating : {value:1,title:"正在認(rèn)證",next:["Authenticated"]}Authenticated : {value:2,title:"已認(rèn)證",next:["Unauthenticated"]},}}}, } ......// 當(dāng)狀態(tài)機(jī)進(jìn)入Connected后會(huì)啟動(dòng)其子狀態(tài)機(jī)// 子狀態(tài)機(jī)會(huì)轉(zhuǎn)換到其初始狀態(tài)Unauthenticated,然后就可以在此執(zhí)行登錄動(dòng)作async onUnauthenticatedEnter({from,to}){this.login() // }onAuthenticated({from,to}){}@state({when:["Authenticating"],pending:["Authenticating"]})async login(){await this.send({// 認(rèn)證信息})}@state({when:["Authenticated"] })async logout(){await this.send({// 注銷信息}) }
}
推薦
以下是我的一大波開源項(xiàng)目推薦:
- 全流程一健化React/Vue/Nodejs國(guó)際化方案 - VoerkaI18n
- 無以倫比的React表單開發(fā)庫 - speedform
- 終端界面開發(fā)增強(qiáng)庫 - Logsets
- 簡(jiǎn)單的日志輸出庫 - VoerkaLogger
- 裝飾器開發(fā) - FlexDecorators
- 有限狀態(tài)機(jī)庫 - FlexState
- 通用函數(shù)工具庫 - FlexTools
- 小巧優(yōu)雅的CSS-IN-JS庫 - Styledfc
- 為JSON文件添加注釋的VSCODE插件 - json_comments_extension
- 開發(fā)交互式命令行程序庫 - mixed-cli
- 強(qiáng)大的字符串插值變量處理工具庫 - flexvars
- 前端link調(diào)試輔助工具 - yald
- 異步信號(hào) - asyncsignal