天天看點

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

簡單說,有限狀态機是一種模型,用來模拟現實世界的事物,但是很多js元件也都可以用有限狀态機來描述,隻要這個元件的行為可以通過幾個有限的狀态來劃分,利用狀态機寫出來的代碼,邏輯思維或者說面向對象思維更加清晰,表達能力更強。

本文是一篇學習性的文章,學習利用有限狀态機的思想來定義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):

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

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

2. 使用javascript-state-machine庫實作狀态機

隻要引入該庫的js之後就能通過該庫提供的一個全局對象StateMachine,并使用該對象的create方法,生成有限狀态機的執行個體(引自該庫官方文檔的交通燈例子):

例1(對應demo1.html):

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

在這個例子中: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() - 以數組的形式傳回執行個體目前狀态下能夠被觸發的行為清單      

在控制台列印這個對象,就可以看到這個對象的所有成員:

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

還記得前面列出的可以用有限狀态機模型的事物特點吧,接下來就用例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方法:

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

當調用warn方法,導緻對象的狀态由green變成yellow之後,panic方法就能調用了。

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

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

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

例2(對應demo2.html):

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

這個例子感覺模拟的是一個人,它的意思表達地很清楚:它模拟的這個人有四個狀态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中我們可以通過下面的方式來定義這四個類型的回調:

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

在浏覽器中打開頁面,在控制台調用一下fsm.eat,可以看到如下的列印結果:

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

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

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);
}      

運作結果:

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

可以在控制台看看這個執行個體的成員:

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

相比例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元件

在實際工作中,肯定會碰到在行為觸發期間,因為某些條件不允許需要取消該行為的情況, 以免對象狀态被錯誤的更改,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回調:

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

不加error回調:

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

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

3. 小結

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

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

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

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

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

謝謝閱讀:)

本文代碼下載下傳

如果您覺得本文對你有用,不妨幫忙點個贊,或者在評論裡給我一句贊美,小小成就都是今後繼續為大家編寫優質文章的動力,流雲拜謝!

歡迎您持續關注我的部落格:)

作者:流雲諸葛

出處:http://www.cnblogs.com/lyzg/

版權所有,歡迎保留原文連結進行轉載:)

繼續閱讀