本文是一篇學習性的文章,學習利用有限狀態機的思想來定義javascript組件的方法,歡迎閱讀,后續計划會寫幾篇專門介紹自己利用有限狀態機幫助自己編寫組件的博客,證明這種思路對於編程實現的價值,目前正在積極構思中。本文代碼下載
1. 有限狀態機概述
簡單說,有限狀態機是一種模型,模型都用來模擬事物,能夠被有限狀態機這種模型模擬的事物,一般都有以下特點:
1)可以用狀態來描述事物,並且任一時刻,事物總是處於一種狀態;
2)事物擁有的狀態總數是有限的;
3)通過觸發事物的某些行為,可以導致事物從一種狀態過渡到另一種狀態;
4)事物狀態變化是有規則的,A狀態可以變換到B,B可以變換到C,A卻不一定能變換到C;
5)同一種行為,可以將事物從多種狀態變成同種狀態,但是不能從同種狀態變成多種狀態。
比如一個模擬復選按鈕的開關組件可以用狀態機這樣描述:
var Switch = function ($elem) { var log = function (fsm, previousState) { console.log('currentState is : ' + fsm.currentState + ((previousState || '') && (' , and previous state is : ' + previousState))); }; return { currentState: 'off', states: { 'on': { to: 'off', action: 'turnOff' }, 'off': { to: 'on', action: 'turnOn' } }, init: function () { var self = this; $elem.on('click', (function () { var args = arguments; return function () { self.transition(args); } })()); log(this); }, transition: function (e) { var old = this.currentState; this.currentState = this.states[old].to; var action = this.states[old].action; (action in this) && this[action](old); }, turnOn: function (fromState) { $elem.addClass('on'); log(this, fromState); }, turnOff: function (fromState) { $elem.removeClass('on'); log(this, fromState); } } };
在這個簡單示例中,Switch組件共有2種狀態,分別是on和off,它要么處於on狀態,要么處於off狀態,初始狀態為off,它有2例行為:turnOff和turnOn,前者能使組件從on狀態變化到off狀態,后者能使組件從off狀態變為on狀態,它的行為綁定到了某個DOM元素的點擊事件上,以下是我用這段js(switch.js)結合jquery運行,點擊按鈕三次之后的結果(對應源碼中的switch.html):
可以看到當調用s.init()之后打印的是這個組件的初始狀態,當點擊一次之后,組件從off狀態轉換到了on狀態,點擊第二次之后從on狀態轉換到了off狀態,點擊第三次又恢復到了on狀態。這個例子雖然是一個極其簡單的狀態機實現,但還是能夠比較恰當地說明狀態機的思想以及它的優點(邏輯思維清晰, 表達能力強)。在實際工作中,我們可以借助javascript-state-machine來實現基於狀態機的組件,它是有限狀態機這種模型的一個js的實現庫,利用它可以快速定義一個狀態機對象,相比我前面舉例寫出的那種實現,這個庫雖然源碼只有200多行,但是功能非常完整,API簡單好用,值得學習跟實踐。
2. 使用javascript-state-machine庫實現狀態機
只要引入該庫的js之后就能通過該庫提供的一個全局對象StateMachine,並使用該對象的create方法,生成有限狀態機的實例(引自該庫官方文檔的交通燈例子):
例1(對應demo1.html):
在這個例子中:initial選項用來表示fsm對象的初始狀態,events選項用來描述fsm對象所有狀態的變化規則,每一種變化規則對應一種行為(不過有可能多個規則會對應同一個行為,在后面你會看到這樣的例子)。create方法為實例的每一種行為都添加了一個方法,調用這個方法就相當於觸發對象的某種行為,當對象行為發生時,對象的狀態就可以發生變化。如以上例子創建的實例將擁有如下行為方法:
fsm.warn() - 調用該方法,實例狀態將從'green'變為'yellow' fsm.panic() - 調用該方法,實例狀態將從'yellow'變為'red' fsm.calm() - 調用該方法,實例狀態將從'red'變為'yellow' fsm.clear() - 調用該方法,實例狀態將從'yellow'變為'green'
這些方法是StateMachine根據create時配置的events規則自動創建的,方法名跟events規則里面的name屬性對應,events規則里面有幾個不重復的name,就會添加幾個行為方法。同時為了方便使用,它還添加了如下成員來判斷和控制實例的狀態和行為:
fsm.current - 返回實例當前的狀態 fsm.is(state) - 如果傳入的state是實例當前狀態就返回true fsm.can(eventName) - 如果傳入的eventName在實例當前狀態能夠被觸發就返回true fsm.cannot(eventName) - 如果傳入的eventName在實例當前狀態不能被觸發就返回true fsm.transitions() - 以數組的形式返回實例當前狀態下能夠被觸發的行為列表
在控制台打印這個對象,就可以看到這個對象的所有成員:
還記得前面列出的可以用有限狀態機模型的事物特點吧,接下來就用例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方法:
當調用warn方法,導致對象的狀態由green變成yellow之后,panic方法就能調用了。
5)同一種行為,可以將事物從多種狀態變成同種狀態,但是不能從同種狀態變成多種狀態
這個例子不能很好的說明這一點,因為它的狀態變化規則里面沒有那種同一個行為,從多種狀態變換到某種狀態的規則,但是這個例子是肯定滿足這一點要求的,因為它的變化規則配置里面,一共定義了4種行為,每種行為都只能從一種狀態變換到另外一種狀態,變換前后都沒有多種狀態的情況。另外從理論上也很好理解這一點,為什么不能從同種狀態變成多種狀態,因為第一點說了事物任一時刻只能處於一種狀態,如果某一個行為使得事物的狀態變成了多種,事物的狀態機制就有問題了。
下面用另外一個官方的例子來說明同一個行為,可以從多種狀態變換到一種狀態的場景:
例2(對應demo2.html):
這個例子感覺模擬的是一個人,它的意思表達地很清楚:它模擬的這個人有四個狀態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種類型的回調:
onbeforeEVENT_NAME - 在EVENT_NAME對應的行為發生之前觸發 onleaveSTATE - 在要改變STATE對應的狀態時觸發 onenterSTATE - 在把當前狀態設置為了STATE對應的狀態時觸發 onafterEVENT_NAME - 在EVENT_NAME對應的行為發生之后觸發
其中,EVENT_NAME都跟據events配置規則里面name,from, to包含的名稱來指定,每個回調都能接收三個參數:
event - 行為名稱 from - 行為發生前的狀態 to - 行為發生后的狀態
狀態機每一個行為觸發后,一定會觸發onbeforeEVENT_NAME和onafterEVENT_NAME這兩個回調,同時行為發生前的狀態對應的onleaveSTATE和行為發生后的狀態對應的onenterSTATE回調也一定會被觸發(只要這些回調都有定義的話),並且回調順序跟前面列出的順序一致。
在例2中我們可以通過下面的方式來定義這四個類型的回調:
在瀏覽器中打開頁面,在控制台調用一下fsm.eat,可以看到如下的打印結果:
根據打印的順序也能看到回調的順序:
onbeforeeat
onleavehungry
onentersatisfied
onaftereat
前面這四個回調對應的是四個類型,state不一樣,或者是event不一樣,需要定義的回調就不同,前面針對的是hungry,satisfied和eat行為定義的回調,還可以針對full,sick和rest行為定義回調,還可以再定義onenterhungry和onleavesatisfied的回調,實際應用里面要定義哪些回調來編寫邏輯代碼,得根據需求而定,javascript-state-machine會根據events規則在相應行為發生時觸發這四類回調。
另外javascript-state-machine還定義了四個通用回調,這四個回調跟event,state沒有關系,在任何行為觸發,任何狀態變化的時候,相關的回調都會觸發,這四個回調是:
onbeforeevent - 在任何行為發生之前觸發 onleavestate - 在要改變對象狀態時觸發 onenterstate - 在把當前狀態設置為新狀態時觸發 onafterevent - 在任何行為發生之后觸發
這四個回調名稱,是固定的,跟觸發的行為和要改變的狀態沒有關系,相當於是全局回調。也就是說,如果某個狀態變化規則相關的四個類型的回調有定義並且這四個全局回調也有定義的話,並且這四個全局回調也有定義的話,那么觸發該規則對應的行為,就一共會觸發8個回調,這8個回調的順序是(以例2中這條規則來說明{name: 'eat', from: 'hungry', to: 'satisfied'}):
onbeforeeat
onbeforeevent
onleavehungry
onleavestate
onentersatisfied
onenterstate
onaftereat
onafterevent
這些回調可以在初始化的時候,通過callbacks選項傳給create來初始化,也能通過直接修改實例的屬性來增加或修改(對應demo3.html):
fsm.onentersatisfied = null; fsm.onleavestate = function(event, from, to) { console.log('狀態變了!,變之前:' + from + ',變之后:' + to); }
運行結果:
可以在控制台看看這個實例的成員:
相比例1打印的交通燈實例的成員,例2實例的成員除了行為方法與例1不同以外,還多出了以on開頭的這些回調成員,例1之所以沒有,那是因為例1沒有用callbacks去配置。
了解到前面這些內容,就可以用javascript-state-machine來改寫前面的開關組件了(對應switch2.html):
var Switch = function ($elem) { var log = function (from, to) { console.log('currentState is : ' + to + ((from || '') && (' , and previous state is : ' + from))); }, fsm = StateMachine.create({ initial: 'off', events: [ {name: 'turnOn', from: 'off', to: 'on'}, {name: 'turnOff', from: 'on', to: 'off'} ], callbacks: { onafterturnOn: function(event, from ,to){ $elem.addClass('on'); log(from, to); }, onafterturnOff: function(event, from, to) { $elem.removeClass('on'); log(from, to); } } } ); ; $elem.on('click', function(){ fsm[fsm.transitions()[0]](); }); log(undefined, fsm.current); return fsm; };
使用方式:
<script src="js/jquery.js"></script> <script src="lib/javascript-state-machine-master/state-machine.js"></script> <script src="js/switch2.js"></script> <script> var s = new Switch($('#btn-switch')); </script>
運行效果還跟之前的一樣:
在實際工作中,肯定會碰到在行為觸發期間,因為某些條件不允許需要取消該行為的情況, 以免對象狀態被錯誤的更改,javascript-state-machine提供了3種方式來取消行為:
在onbeforeEVENT_NAME回調中return false可以取消當前觸發的行為
在onleaveSTATE回調中return false也可以取消當前觸發的行為
在onleaveSTATE回調中return StateMachine.ASYNC來執行異步的行為
前兩種方法,在指定的回調中return false即可取消行為,第三個方法返回的僅是一個異步標識,是否取消行為需要在異步任務的回調里面進一步指定。這個方法適用於那些帶有異步任務的行為,就是說在這種行為觸發的時候,並不是同時就觸發對象狀態的改變,而是要等到異步任務執行完成之后再改變狀態,引用官方的例子來說明這種異步任務的場景:
var fsm = StateMachine.create({ initial: 'menu', events: [ { name: 'play', from: 'menu', to: 'game' }, { name: 'quit', from: 'game', to: 'menu' } ], callbacks: { onentermenu: function() { $('#menu').show(); }, onentergame: function() { $('#game').show(); }, onleavemenu: function() { $('#menu').fadeOut('fast', function() { fsm.transition(); }); return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in fadeOut callback above) }, onleavegame: function() { $('#game').slideDown('slow', function() { fsm.transition(); }; return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in slideDown callback above) } } });
這個例子中創建的實例,包含play和quit兩個行為,這兩個行為觸發之后,不會立即去更改對象的狀態,而是開啟一個異步的動畫任務,然后在動畫結束之后,通過調用實例的transition方法:fsm.transition(),通知實例去改變自己的狀態。為了告訴fsm,當前執行的是一個帶異步的行為,需要在onleaveSTATE回調中,如onleavemenu, onleavegame,通過return StateMachine.ASYNC來處理。另外,在異步任務結束的回調里面,如果想要fsm更改狀態,就通過fsm.transition()去通知它;但是如果在異步任務結束之后,由於有些條件不允許,還是想取消這個行為的話,可以改成調用fsm.cancel()來通知它,這樣fsm就會取消當前的異步行為,對象狀態也不會改變。
這種異步編程的方式跟jquery的延遲對象的做法是類似的:
function foo(url){ var defer = $.Deferred(); $.ajax({ url: url }).done(function(res){ defer[res.code == 200 ? 'resolve' : 'reject'](); }).fail(function(){ defer.reject(); }); return $.when(defer) }
最后關於javascript-state-machine還可以在本文說明一下的就是error這個選項,在create實例的時候,可以通過這個選項來指定一個回調,這樣在觸發了在當前狀態不該觸發的行為時,fsm不會拋出錯誤,而是把這個錯誤交給error指定的回調來處理,否則它就會直接把錯誤拋給瀏覽器,這肯定會導致組件的功能無法使用,所以如果要用javascript-state-machine,這個回調一定要加上,哪怕只是簡單打印一些信息(對應demo4.html):
加error回調:
運行結果:
不加error回調:
運行結果:
有關javascript-state-machine的用法介紹到此結束,在官方文檔中還有2個小節也有用得着的場景,對這個庫感興趣的話推薦再去學習官方文檔~
3. 小結
1)有限狀態機是定義組件的一種好用的模式,能夠讓組件的代碼看起來更加清晰,而且易於理解;
2)javascript-state-machine也是一個優秀的實現庫,源碼簡潔,提供的API用法簡單,同時還突出了狀態機的特點,值得在定義組件的時候去試一試;
3)有限狀態機這種模式適合有明顯狀態特點的組件;
4)在使用javascript-state-machine的時候,既可以直接在fsm的基礎上定義組件,也可以在組件內部通過一個私有成員來保留一個fsm(內部狀態機);
5)本文所舉的例子不夠貼近實際項目,近期會看看自己做過的項目中有哪些適合用狀態機模式來重寫的模塊,到時候再寫博客來與大家分享。
謝謝閱讀:)