初探 Finite State Machine 與 XState-提升程式碼的可讀性

YU-EN TUNG
AsiaYo Engineering
Published in
9 min readMar 9, 2022

前言

每位前端工程師在完成同樣一個元件時,由於實作的方式不盡相同,都會需要花費一些時間理解彼此的做法,在相對複雜的元件上更是如此。除此之外,如果你/妳和我一樣是使用 React 進行開發,會發現若是單單使用 useState 進行狀態管理,狀態變化的邏輯總是四散在檔案各處,即便使用 useReducer 統一將狀態變化集中撰寫,狀態改變時發生的 side effect 也同樣散落在其他地方,這都讓往後回頭理解時需要自行拼湊出完整的邏輯。為了讓團隊更順利合作,我們嘗試使用讓彼此更容易理解的方式來撰寫。

秉持著 give it a try 的精神,這次嘗試使用新接觸到的 XState 來 refactor 現有的幾個元件,並比較看看前後的差異。同時,在暸解 XState 背後的概念 — finite state machine 與 statechart 後,發現這便是一套現成的規範可供我們使用。此外,XState 已經實作了相關的 API 讓我們能夠快速建構出它們,也幫助我們將所有邏輯集中起來,方便之後理解。

接下來是我在理解 XState 的過程中整理出來的幾個重點,在這邊與大家分享~

Finite State Machine、Statechart 與 XState

Finite state machine 與 statechart 是一套模型,在這套模型裡面主要包含了幾個部份,像是有哪些狀態(states)、事件(events) 與轉換函式(transition function) 等等。換句話說,如果想建構出一個 finite state machine 與 statechart,我們需要根據規範定義出 states、events 與 transition function 等等,待會會針對這幾個部分一一介紹。而 XState 則是一個 JavaScript 的套件,它根據 finite state machine 與 statechart 的規範設計了一系列 API 供我們使用,讓我們能夠更快速地建構出它們。

接下來會以 AsiaYo 平台上的收藏按鈕 為例,和大家分享在建構 finite state machine 與 statechart 的過程中需要定義哪些東西,以及如何使用 XState 所提供的 API。

States

首先,我們需要定義出有限數量的狀態,以收藏按鈕來看,會有喜歡(favorite) 與不喜歡(unFavorite) 兩種狀態,再加上在這兩種狀態間切換時會有一個 loading 的狀態,所以總共會有三種狀態。

如果是使用 XState 來定義的話,我們需要使用 createMachine 這個 function,並提供一個 JavaScript 的物件,在之中使用 states 的 key 進行定義即可。

Events 與 Transition Function

接著,我們需要定義出事件與 transition function。事件的意思是當狀態有所改變的時候,需要藉由事件來觸發,而 transition function 則會根據當前的狀態以及發生的事件來決定下一個狀態是什麼,就像下面這個函式。

舉例來說,如果目前的狀態是 favorite,這時候如果發生了 TOGGLE 事件,transition function 就會回傳 loading 的狀態。

在 XState 中,我們需要在各個狀態中,以 on 這個 key 來定義各種不同的事件,並以 target 來表示事件發生以後會轉變成什麼狀態。

Initial State 與 Final State

最後,我們需要定義初始狀態與最終狀態(如果有的話),以收藏按鈕來看,初始狀態會是 unFavorite。由於這個例子不包含最終狀態,所以不需要特別定義。

在 XState 中,如果需要定義初始狀態,將初始狀態設置為 initial 這個 key 的 value 即可。

當我們定義好了上面提到的幾個部分,也就將 finite state machine 建構完成了。可以發現 finite state machine 只定義了像 states、events 等等基本的項目,而 statechart 則提出了一些延伸的部分,像是狀態改變時伴隨發生了什麼 side effect 等等,這邊也和大家分享幾個比較常見的概念。

Guarded Transition

Guarded transition 其實就是 transition function 的延伸,從前面的描述可以知道,transition function 會根據當前的狀態以及發生的事件來決定下一個狀態是什麼,而 guarded transition 則是當事件發生的時候,會再經過一些條件上的判斷,來決定接下來的狀態。

舉例來說,當使用者點擊收藏按鈕的時候,我們需要判斷使用者的登入狀態,只有在登入的情況下才能夠進到 loading 的狀態之中,若是沒有登入則只能夠繼續待在 unFavorite 的狀態裡,這時候就適合使用 guarded transition。

在 XState 中,如果想使用 guarded transition 我們需要使用 cond 這個 key 來進行額外的判斷。

Extended State(Context)

前面主要都是在說明狀態之間的改變,但有可能狀態改變的時候伴隨了其他內容上的改變,這時候就很適合使用 context 來做紀錄。順帶一提,context 的另一個名稱是 extended state,也就表示它是從 state 衍生出來的項目。

像是點擊收藏按鈕時,畫面的左下角會跳出提示訊息,在不同的情境下會顯示出不同的提示文字,這種情況就很適合使用 context 來紀錄。

在 XState 裡,我們可以將各個 context 定義在 context 這個 key 之中。

Actions

當狀態發生改變的時候,可能會伴隨其他 side effect 的發生,比方說狀態由 favorite 轉變至 unFavorite 時,需要改變顯示出來的提示文字,又或是需要做一些 google event 追蹤等等,這時候便可以使用 action。

XState v.s useState、useReducer

最後,想和大家分享透過 XState(finite state machine 與 statechart) 的方式管理狀態和使用 useState、useReducer 之間的差異。

最主要的差異在於,當我們使用 XState 時,我們會將狀態之間的變化、狀態變化時所發生的 side effect 一併寫在 createMachine 裡面。

相反地,如果在狀態複雜的情況下,單純使用多個 useState 管理狀態,各個狀態變化的邏輯便會四散在檔案各處,需要自行拼湊出完整的邏輯。

而使用 useReducer 雖然可以達到集中管理各項狀態間的變化,但並未區分 state 與 context 之間的差異,容易複雜化狀態本身,導致可讀性下降。

除此之外,無論是 useState 或是 useReducer 都只關注在狀態上的改變,而狀態改變時伴隨的 side effect 需要額外再透過 useEffect 進行管理,與 XState 將 side effect 統一集中管理的方式相比,在理解時也會需要花費比較多的時間自行拼湊出完整的邏輯。

XState 有再提供了一個 視覺化工具,可以幫助我們畫出狀態圖,大大降低理解的時間,也能夠方便我們與設計師、PM 一同討論。

雖然說使用 XState 管理狀態會帶來上述好處,但也有一些潛在的缺點是需要評估的。XState 可以把所有內容都整合到一個 JavaScript 的物件裡面,但當狀態越複雜導致這個物件太過龐大,反倒會讓可讀性變差,這時候便需要思考如何有效地拆分檔案。

另外,如果採用 finite state machine 的方式來管理狀態,一開始在建構出整個狀態的藍圖時,可能會需要團隊成員一同討論,討論過程中是否投入太多時間成本在這邊,也是需要評估的地方。

總結

最後做個簡單的總結,XState 最主要的用處是讓我們可以更容易地建構出符合 finite state machine 與 statechart 的狀態模型,而透過 finite state machine 與 statechart 這種更嚴謹的方式管理狀態,則能夠提升程式碼的可讀性。

以上是這次有關於 finite state machine、statechart 與 XState 簡單的分享,謝謝大家的收看~

最後的最後,目前 AsiaYo 正在擴編,我們正在徵求

  • QA Engineer
  • Product Manager
  • Backend Engineer
  • Frontend Engineer
  • UI/UX Designer
  • Site Reliability Engineer

詳細資訊在 CakeResume 上,歡迎大家成為我們的夥伴!

--

--