有限狀態機是簡化的運算模型,在前端頁面需求日益複雜的場合,我們常見到採用有限狀態機模型的例子,但「霧裡看花不是花」,既然如此,不如直接探討有限狀態機,對前端狀態管理才能有根本認識。

有限狀態機!==狀態圖

在Flux、Redux等架構提出後,狀態管理這件事在前端領域,成了熱門議題之一,關於「有限狀態機」這個名詞,也經常穿插在前端相關的文件之中,使用者介面上,各式元件的現況被描述為狀態,事件被描述為驅動狀態變化的規則,然後將這一切畫成狀態圖,接著以相關程式庫或框架實現,只是,為什麼要用這些框架?如果只是想實現狀態圖,單純的DOM操作不也可以嗎?

正如從命令式語言加上一些限制來理解函數式,終究只是個取巧方式,從純函數式語言中,才能看清楚真貌,如果想善用有限狀態機來進行狀態管理,最好的方式就是探討「有限狀態機」這個運算模型,也就是運算原理中最簡單的電腦模型,它被剝去複雜功能,以便從簡化的模型中理解、掌握運算本質。

有限狀態機中的「狀態」是運算模型啟始後,接受一連串「輸入」後展現的現況描述,例如,0或1是兩個狀態,系統啟始時會處於初始狀態,像是狀態0,「規則」決定了系統處於哪個狀態下,接受哪個輸入會轉移至哪一狀態,像是「狀態0輸入1後,變為狀態1」是條規則,「狀態1輸入1後,變為狀態0」是另一條規則;而且,規則之間必須沒有矛盾,也就是:同一輸入不能有兩個可套用的規則,規則必須沒有遺漏,以及對於每個輸入都必須有能套用的規則。

根據方才的說明,其實,我們已經建立了一個有限狀態機,在運算原理中,對於有限狀態機的探討,會從這類簡單狀態機開始,逐步建立複雜的有限狀態機,或者組合多個子狀態機來達成複雜任務;進一步來看,我們則會探討非確定性有限狀態機的運算,以及如何透過狀態合併,把非確定性有限狀態機轉換為等價的有限狀態機。

關於有限狀態機的狀態圖?那只不過是以視覺化方式來表現有限狀態機罷了,如果沒能夠定義狀態、輸入、規則,沒能逐步建構、組合出複雜狀態機,沒能夠合併狀態……,是否畫出狀態圖,差別不過是文件中有沒有多張圖罷了。

例如,撰寫規則表示式時,實際上,就是一種有限狀態機的設計過程,規則表示式能否比對出目標文字,重點在於能否掌握文字的規則、字元對應至哪個子表示式,以及如何組合子表示式成為更完整的表示式,在設計規則表示式時,應該很少有開發者會畫出狀態圖吧!

有限狀態機與函數式

狀態管理這件事也不只是前端才會面臨的問題,後端為了執行緒、同步等議題,早就做過類似的探討,狀態管理之所以複雜,就是因為狀態會變動,既然如此,那就堵死這條路,令狀態不會變動,就不會有狀態管理的問題。而就函數式語言來說,出發點就是沒有變數、資料內容不可變的概念。

如果具有變數的概念,一旦變數橫跨了整個函式,而函式本體又非常冗長的話,很容易就在某個不經意的地方被修改;若一組資料狀態是可變的,在應用程式規模擴大之後,資料可能在任何時間、地點被修改,但方才談到「有限狀態機的規則必須沒有遺漏」,可變動的特性會令規則包含時間、地點作為輸入,若難以掌握這類輸入,就會令規則產生遺漏,從而難以掌握應用程式會進入哪個狀態。

函數式的特色就是狀態不隨著時間、地點而改變,狀態不會變動這件事,並不是指整個應用程式狀態不會變,而是指函式在運算過程中,不用擔心收到的狀態與輸入,莫名地被改變了,既有的狀態與輸入,必然對應至另一個新建立的狀態。

如果不是特別從事某些類型的程式開發,例如,數位電路、編譯器、網路協定等,過去開發者很少會接觸到有限狀態機,這是因為,命令式語言不限制開發者修改變數,開發者沒有意會到:在命令式語言修改變數值,實際上就是在產生一個新狀態。

在函數式語言中,因為無法修改變數值,就會強制開發者抽離函式——大功能的函式,往往包含數個狀態變化,最後必定被迫抽離出數個子函式來組合。而在有限狀態機中,規則將目前狀態與輸入,映射至另一個狀態,這就像是函式,而將一個大函式分解為數個小函式的組合,就像是將大狀態機分解為數個小狀態機的組合,最後資料就像是從一個函式流往另一個函式,每經一個函式,就會是建立新的狀態,就如同狀態機中,每經一個規則,就會到達一個狀態。

狀態物件、狀態樹

對於前端來說,頁面呈現就是DOM樹的狀態可視化,在頁面與行為不複雜的情況下,修改頁面呈現的方式,就是修改DOM樹中很小的一部份,情況容易控制,也不需要用上狀態管理框架;然而DOM樹就是顆全域樹,在頁面與行為複雜之後,應用程式在任何時間、地點,都可以修改這棵全域樹的話,狀態與規則就會複雜化,漸漸地失去對DOM樹的狀態控制。

既然修改全域樹是個問題,那麼,就別修改它。開發者準備狀態物件,宣告狀態物件綁定在哪些元件,以及哪些事件會怎麼改變狀態物件,接著,由框架來拉取狀態、改變DOM樹(而不是開發者直接修改)。

這麼一來,原本附著在DOM樹上,與應用程式相關的各個狀態就要剝離出來,成為狀態物件,以便綁定至元件,開發者不用再管理整個DOM樹,而是管理各個狀態物件。

頁面中的每個元件是各個狀態的表現,如果狀態不需要共享,就像各自元件擁有各自的狀態機,若狀態需要在多個元件之間共享,那麼,這些狀態機就需要以某種方式聯繫起來,例如,透過父元件來共享狀態,多個子元件的操作都會影響共享的狀態,管理的狀態範圍就又會開始擴大,因此,開發者必須知道父子元件間的行為,才能理解為什麼父子元件會如此呈現。

如果經常需要共享狀態,那就來個唯一的全域狀態樹吧!這個全域樹是從DOM樹中剝離出來,可由開發者管理,為了避免規則中包含時間、地點作為輸入,令其成為唯讀的全域樹,運用純函數式的設計方式,開發者可以將全域樹分解為更小的狀態樹,每個新的子狀態樹都是由小狀態機來定義,而每一次的操作都會產生新的全域狀態樹,這會是小狀態機組合而成的巨大狀態機成果,也是之前專欄〈可預期的狀態管理〉中談到,Redux希望開發者去管理的事。

認識有限狀態機

有機會的話,開發者應該試著去理解運算原理中,對於有限狀態機的探討。看看在純粹的數學模型下,會是如何思考有限狀態機,閱讀《深入理解運算原理》第三章是個不錯的出發點,它是個可以獨立閱讀的章節。

在這之後,試著從有限狀態機的角度,來看看函數式及前端的狀態管理,而不是從語法、API,或者是MV-Whatever的角度,來看待語言或理解相關框架,這麼一來,你就會知道,真正能管理狀態的,其實是開發者本身,而不是這類工具。

專欄作者

熱門新聞

Advertisement