試試用有限狀態機的思路來定義javascript元件

發表於2016-03-08

本文是一篇學習性的文章,學習利用有限狀態機的思想來定義javascript元件的方法,歡迎閱讀,後續計劃會寫幾篇專門介紹自己利用有限狀態機幫助自己編寫元件的部落格,證明這種思路對於程式設計實現的價值,目前正在積極構思中。本文程式碼下載

1. 有限狀態機概述

簡單說,有限狀態機是一種模型,模型都用來模擬事物,能夠被有限狀態機這種模型模擬的事物,一般都有以下特點:

1)可以用狀態來描述事物,並且任一時刻,事物總是處於一種狀態;

2)事物擁有的狀態總數是有限的;

3)通過觸發事物的某些行為,可以導致事物從一種狀態過渡到另一種狀態;

4)事物狀態變化是有規則的,A狀態可以變換到B,B可以變換到C,A卻不一定能變換到C;

5)同一種行為,可以將事物從多種狀態變成同種狀態,但是不能從同種狀態變成多種狀態。

比如一個模擬核取按鈕的開關元件可以用狀態機這樣描述:

在這個簡單示例中,Switch元件共有2種狀態,分別是on和off,它要麼處於on狀態,要麼處於off狀態,初始狀態為off,它有2例行為:turnOff和turnOn,前者能使元件從on狀態變化到off狀態,後者能使元件從off狀態變為on狀態,它的行為繫結到了某個DOM元素的點選事件上,以下是我用這段js(switch.js)結合jquery執行,點選按鈕三次之後的結果(對應原始碼中的switch.html):

image

可以看到當呼叫s.init()之後列印的是這個元件的初始狀態,當點選一次之後,元件從off狀態轉換到了on狀態,點選第二次之後從on狀態轉換到了off狀態,點選第三次又恢復到了on狀態。這個例子雖然是一個極其簡單的狀態機實現,但還是能夠比較恰當地說明狀態機的思想以及它的優點(邏輯思維清晰, 表達能力強)。在實際工作中,我們可以藉助javascript-state-machine來實現基於狀態機的元件,它是有限狀態機這種模型的一個js的實現庫,利用它可以快速定義一個狀態機物件,相比我前面舉例寫出的那種實現,這個庫雖然原始碼只有200多行,但是功能非常完整,API簡單好用,值得學習跟實踐。

2. 使用javascript-state-machine庫實現狀態機

只要引入該庫的js之後就能通過該庫提供的一個全域性物件StateMachine,並使用該物件的create方法,生成有限狀態機的例項(引自該庫官方文件的交通燈例子):

例1(對應demo1.html):

image

在這個例子中:initial選項用來表示fsm物件的初始狀態,events選項用來描述fsm物件所有狀態的變化規則,每一種變化規則對應一種行為(不過有可能多個規則會對應同一個行為,在後面你會看到這樣的例子)。create方法為例項的每一種行為都新增了一個方法,呼叫這個方法就相當於觸發物件的某種行為,當物件行為發生時,物件的狀態就可以發生變化。如以上例子建立的例項將擁有如下行為方法:

這些方法是StateMachine根據create時配置的events規則自動建立的,方法名跟events規則裡面的name屬性對應,events規則裡面有幾個不重複的name,就會新增幾個行為方法。同時為了方便使用,它還新增了如下成員來判斷和控制例項的狀態和行為:

在控制檯列印這個物件,就可以看到這個物件的所有成員:

image

還記得前面列出的可以用有限狀態機模型的事物特點吧,接下來就用例1來說明javascript-state-machine建立的物件是如何滿足狀態機模型的要求的:

1)可以用狀態來描述事物,並且任一時刻,事物總是處於一種狀態

這個例子中建立的交通燈例項,要麼處於yellow狀態,要麼處於red狀態,要麼處於green狀態,所以它是滿足第1點的。

2)事物擁有的狀態總數是有限的

這個例項最多隻有三個狀態。

3)通過觸發事物的某些行為,可以導致事物從一種狀態過渡到另一種狀態

fsm.warn,fsm.panic,fsm.cal,fsm.clear這幾個行為方法都能改變例項的狀態。

4)事物狀態變化是有規則的,A狀態可以變換到B,B可以變換到C,A卻不一定能變換到C

這個例項的初始狀態為green,根據events配置的狀態變化規則,green可以變換到yellow, yellow可以變換到red,但是例項初始化之後,卻不能呼叫fsm.panic這個行為方法,因為這個方法只有例項狀態為yellow的時候才能呼叫,而初始化時例項的狀態為green,所以一開始只能呼叫warn方法:

image

當呼叫warn方法,導致物件的狀態由green變成yellow之後,panic方法就能呼叫了。

5)同一種行為,可以將事物從多種狀態變成同種狀態,但是不能從同種狀態變成多種狀態

這個例子不能很好的說明這一點,因為它的狀態變化規則裡面沒有那種同一個行為,從多種狀態變換到某種狀態的規則,但是這個例子是肯定滿足這一點要求的,因為它的變化規則配置裡面,一共定義了4種行為,每種行為都只能從一種狀態變換到另外一種狀態,變換前後都沒有多種狀態的情況。另外從理論上也很好理解這一點,為什麼不能從同種狀態變成多種狀態,因為第一點說了事物任一時刻只能處於一種狀態,如果某一個行為使得事物的狀態變成了多種,事物的狀態機制就有問題了。

下面用另外一個官方的例子來說明同一個行為,可以從多種狀態變換到一種狀態的場景:

例2(對應demo2.html):

image

這個例子感覺模擬的是一個人,它的意思表達地很清楚:它模擬的這個人有四個狀態hungry, satisfied, full ,sick,分別代表餓了,高興,飽了,病了,初始狀態為hungry,這個人有2種行為eat和rest,分別代表吃和休息,只要這個人一開始吃,它的狀態就由餓了變成高興(人餓的時候有東西吃可不得高興),再吃的話,狀態就由高興變為飽了,要是吃多了的話,這個人就會生病;不管這個人是餓是飽,是高興還是得病,只要是在那躺著不動休息,最終都會餓。跟例1不同的是,這個例子:

1)雖然它配置了多個變化規則,但是它只有2個行為(events配置中有多少個不重複的name(值),就表示這個狀態機有多少個行為);

2)它的eat行為發生後的狀態跟當前狀態有關係,當前狀態不同,行為發生後的狀態也不同,所以eat行為對應了多條配置規則;

3)它的rest行為發生後的狀態跟當前狀態沒關係,只要當前狀態在rest行為的狀態條件範圍內,行為發生後的結果都是一樣的,所以rest行為用一個from陣列配置了該行為發生的當前狀態的條件範圍,整個行為僅定義了一條配置規則。

在實際使用狀態機例項的過程中,我們通過呼叫例項的行為方法來觸發例項狀態的改變,比如例1中: fsm.warn(),這樣fsm的狀態就會由green變為yellow,像這種簡單的狀態機例項,這個程度的使用也許就足夠了,但是對於實際專案而言,我們定義的元件,往往要用它們生成的例項來完成很多複雜的邏輯功能,如果用狀態機來定義元件,那麼這些邏輯程式碼該寫在哪裡?因為javascript-state-machine建立的狀態機例項,它的行為方法都是自動新增的,你不可能去重寫這些行為方法,否則就失去狀態機的意義了(將狀態變化的邏輯與業務邏輯拆分)。答案是回撥。javascript-state-machine為每個例項的每種狀態的變換前後和每種行為的變換前後都定義了相關的回撥,你的邏輯都可以寫在這些回撥裡面,這樣就達到了狀態邏輯與業務邏輯拆分的目的。下面先看看這些回撥的用法,接著我會用javascript-state-machine改寫一下前面那個模擬核取方塊的開關元件的例子。

javascript-state-machine根據events的配置,可以為例項定義4種型別的回撥:

其中,EVENT_NAME都跟據events配置規則裡面name,from, to包含的名稱來指定,每個回撥都能接收三個引數:

狀態機每一個行為觸發後,一定會觸發onbeforeEVENT_NAME和onafterEVENT_NAME這兩個回撥,同時行為發生前的狀態對應的onleaveSTATE和行為發生後的狀態對應的onenterSTATE回撥也一定會被觸發(只要這些回撥都有定義的話),並且回撥順序跟前面列出的順序一致。

在例2中我們可以通過下面的方式來定義這四個型別的回撥:

image

在瀏覽器中開啟頁面,在控制檯呼叫一下fsm.eat,可以看到如下的列印結果:

image

根據列印的順序也能看到回撥的順序:

前面這四個回撥對應的是四個型別,state不一樣,或者是event不一樣,需要定義的回撥就不同,前面針對的是hungry,satisfied和eat行為定義的回撥,還可以針對full,sick和rest行為定義回撥,還可以再定義onenterhungry和onleavesatisfied的回撥,實際應用裡面要定義哪些回撥來編寫邏輯程式碼,得根據需求而定,javascript-state-machine會根據events規則在相應行為發生時觸發這四類回撥。

另外javascript-state-machine還定義了四個通用回撥,這四個回撥跟event,state沒有關係,在任何行為觸發,任何狀態變化的時候,相關的回撥都會觸發,這四個回撥是:

這四個回撥名稱,是固定的,跟觸發的行為和要改變的狀態沒有關係,相當於是全域性回撥。也就是說,如果某個狀態變化規則相關的四個型別的回撥有定義並且這四個全域性回撥也有定義的話,並且這四個全域性回撥也有定義的話,那麼觸發該規則對應的行為,就一共會觸發8個回撥,這8個回撥的順序是(以例2中這條規則來說明{name: ‘eat’, from: ‘hungry’, to: ‘satisfied’}):

這些回撥可以在初始化的時候,通過callbacks選項傳給create來初始化,也能通過直接修改例項的屬性來增加或修改(對應demo3.html):

執行結果:

image

可以在控制檯看看這個例項的成員:

image

相比例1列印的交通燈例項的成員,例2例項的成員除了行為方法與例1不同以外,還多出了以on開頭的這些回撥成員,例1之所以沒有,那是因為例1沒有用callbacks去配置。

瞭解到前面這些內容,就可以用javascript-state-machine來改寫前面的開關元件了(對應switch2.html):

使用方式:

執行效果還跟之前的一樣:

image

在實際工作中,肯定會碰到在行為觸發期間,因為某些條件不允許需要取消該行為的情況, 以免物件狀態被錯誤的更改,javascript-state-machine提供了3種方式來取消行為:

前兩種方法,在指定的回撥中return false即可取消行為,第三個方法返回的僅是一個非同步標識,是否取消行為需要在非同步任務的回撥裡面進一步指定。這個方法適用於那些帶有非同步任務的行為,就是說在這種行為觸發的時候,並不是同時就觸發物件狀態的改變,而是要等到非同步任務執行完成之後再改變狀態,引用官方的例子來說明這種非同步任務的場景:

這個例子中建立的例項,包含play和quit兩個行為,這兩個行為觸發之後,不會立即去更改物件的狀態,而是開啟一個非同步的動畫任務,然後在動畫結束之後,通過呼叫例項的transition方法:fsm.transition(),通知例項去改變自己的狀態。為了告訴fsm,當前執行的是一個帶非同步的行為,需要在onleaveSTATE回撥中,如onleavemenu, onleavegame,通過return StateMachine.ASYNC來處理。另外,在非同步任務結束的回撥裡面,如果想要fsm更改狀態,就通過fsm.transition()去通知它;但是如果在非同步任務結束之後,由於有些條件不允許,還是想取消這個行為的話,可以改成呼叫fsm.cancel()來通知它,這樣fsm就會取消當前的非同步行為,物件狀態也不會改變。

這種非同步程式設計的方式跟jquery的延遲物件的做法是類似的:

最後關於javascript-state-machine還可以在本文說明一下的就是error這個選項,在create例項的時候,可以通過這個選項來指定一個回撥,這樣在觸發了在當前狀態不該觸發的行為時,fsm不會丟擲錯誤,而是把這個錯誤交給error指定的回撥來處理,否則它就會直接把錯誤拋給瀏覽器,這肯定會導致元件的功能無法使用,所以如果要用javascript-state-machine,這個回撥一定要加上,哪怕只是簡單列印一些資訊(對應demo4.html):

加error回撥:

image

執行結果:

image

不加error回撥:

image

執行結果:

image

有關javascript-state-machine的用法介紹到此結束,在官方文件中還有2個小節也有用得著的場景,對這個庫感興趣的話推薦再去學習官方文件~

3. 小結

1)有限狀態機是定義元件的一種好用的模式,能夠讓元件的程式碼看起來更加清晰,而且易於理解;

2)javascript-state-machine也是一個優秀的實現庫,原始碼簡潔,提供的API用法簡單,同時還突出了狀態機的特點,值得在定義元件的時候去試一試;

3)有限狀態機這種模式適合有明顯狀態特點的元件;

4)在使用javascript-state-machine的時候,既可以直接在fsm的基礎上定義元件,也可以在元件內部通過一個私有成員來保留一個fsm(內部狀態機);

5)本文所舉的例子不夠貼近實際專案,近期會看看自己做過的專案中有哪些適合用狀態機模式來重寫的模組,到時候再寫部落格來與大家分享。

謝謝閱讀:)

本文程式碼下載

相關文章