關於Redux,它是個設計架構,透過函數式的概念,以及Action、Reducer、Store等角色的宣告,對於狀態的變化施加限制,而ReduxJS框架則是這類限制的實現,正如官方網站首頁所言,目的在成為可預期的狀態容器(Predictable state container)。

宣告式的Redux

ReduxJS使用上不難,官方網站的〈Getting Started〉列出的第一個基本範例,就包含了重要的Action、Reducer、Store等角色。開發者宣告Action物件,type特性的值表示狀態轉移指令,例如{type: 'INCREMENT'},若有必要,可以帶有其他特性,表示執行指令時的必要資料,多看幾個範例,就可以看出一件事:雖然名為Action,然而,它並不會改變狀態。

狀態之間的關係要宣告Reducer來建立,具體來說,是實作一個接受狀態與Action物件的函式。基本上,Reducer的職責之一,是在沒有收到任何狀態物件時,建立初始狀態;其職責之二,是根據傳入的狀態,以及Action物件的type等特性,建立、傳回新的狀態物件。

Reducer必須是純函式(Pure function),在官方文件〈Reducers〉中談到,開發者不應該在Reducer中改變傳入的引數、執行有副作用的流程、呼叫不純的函式,如果開發者曾接觸過函數式設計,在使用ReduxJS時,馬上就會意會到:Redux架構與函數式設計有著濃厚的關係。

Action是宣告指令與輸入資料的地方,Reducer是宣告狀態轉移規則的場合,想要能宣告Action或Reducer,還有個前提,那就是,要能規畫出各個狀態的外觀。因為,函數式的設計就是宣告式的設計,想要進行宣告式設計的前提,就是要能清楚地定義對象。

其實,這是一種限制,避免開發者一上來就急就章地胡亂撰寫程式,而是強制開發者應先行思考、規畫。

強制開發者的地方還不只有Action與Reducer,應用程式中各處的狀態,必須想辦法組合為唯一的一棵狀態樹,而且,這棵樹是唯讀的。

如方才所言,Reducer會產生新的狀態物件(而不是修改傳入的狀態物件),每個Action處理後,Reducer會產生新的狀態物件,形成這些狀態物件狀態樹的子樹,也就是說,想要使用Redux架構,開發者必須規畫出整個應用程式的狀態,並據此以宣告狀態樹。

有限狀態機的Redux

既然Reducer應該是純函式,那麼,開發者想問了:想顯示目前時間這類有副作用的需求怎麼辦?總得呼叫Date.now之類的吧?開發者應該準備Action前呼叫這類API,取得資料後、令其為Action物件的特性,然後用Store的dispatch送出Action。

Store用來取得、更新狀態,若對狀態變更感興趣,也是對Store進行註冊,以便新的狀態樹建立時收到通知。建立Store時,必須指定root reducer,指定Action是透過Store的dispatch方法,Store把Action、Reducer與狀態結合起來,從技術面來看,Store是個狀態容器。

實際上,Store是個有限狀態機,Reducer中宣告了有限狀態機的規則,Action宣告了有限狀態機需要的指令與資料,dispatch方法就是供給狀態機指令與資料的入口,使用Redux就是要建立有限狀態機,必須清楚地規畫出狀態機中會有哪些狀態,以及狀態之間的轉移規則,而ReduxJS就是個用來實現有限狀態機的框架。

這也說明了為何實作Reducer時,必須比對type特性的值(通常使用switch比對)採取對應操作,而不是傳入一個Command物件採用多型。

若是傳入Command,開發者必須定義如何(How)改變狀態樹,開發者也可能在各處定義出改變狀態的不同操作;如果對type進行比對,開發者必須宣告type對應的狀態轉移是什麼(What),而且,這些宣告會集中在Reducer,因此,若想知道前一個狀態從何而來,下一個狀態會有哪些,只需查看Reducer,就可以知曉。

Reducer為何叫Reducer?

有限狀態機中,規則的設計是一大重點,當狀態數量變得多而複雜時,必須獨立地設計多個有限狀態機,然後將它們組合起來;而採用Redux,就是在設計有限狀態機,因此,設計Action與Reducer時,應該只關心type對應的前、後狀態各是什麼,這也就是為什麼Reducer總是建立新的狀態物件,而不是修改傳入的狀態物件。

這麼一來,各個Action與Reducer就可以只宣告感興趣的子狀態樹轉移,而不是思考整棵狀態樹,之後,必要時,也可以只針對感興趣的Action、Reducer,進行查看或修改。至於Reducer的組合,在實作上,ReduxJS提供了一個combineReducers函式來協助。

這麼一來,就有趣了,由於Reducer是純函式,易於進行測試,各個子狀態樹轉移的Action與Reducer,可以獨立地進行測試,所以,如果想測試某個子狀態樹,Reducer被combineReducers組合之後,傳回的也會是個Reducer,因此,想進行子狀態樹測試,也不是問題。

想要測試整棵狀態樹?那麼,就用root reducer。如果想測試使用者一連串操作後,狀態樹是否符合預期,那麼,就可以準備一連串的Action物件來進行測試。

這就是為何Reducer被稱為Reducer的理由。在官方文件〈Reducers〉談到,之所以會這麼稱呼,是因為:它與Array的reduce方法接受的reducer函式,其實,都是同類的函式。

首先,在簽署上,兩者彼此是相似的,因為Array的reducer,是(accumulatedValue, nextItem) => nextAccumulatedValue,而Redux的reducer,則是(state, action) => newState。

進一步地,如果使用Array的reduce時,要有初始的accumulatedValue,而Reducer在宣告時,也要提供初始狀態;Array會利用reducer將一組元素歸結為單一結果,Redux則利用Reducer將一連串的Action,歸結為某一狀態,開發者易於對陣列元素歸結的單一結果進行測試,對於一連串Action歸結後的某一狀態,也易於測試。

何謂可預期?

ReduxJS官方網站自稱為Predictable狀態容器,其中的狀態容器,代表著框架本身技術層面上的職責,而Predictable在許多文件當中,通常會譯為可預測──一般而言,預測這兩個字往往帶著機率成份,如果對應用程式的狀態掌握帶著機率成份,那會是一場災難吧!

正因為Predictable應該是可預期的、如同先前所規畫的,換言之。在遵守框架的限制與建議下,開發者必須先思考、規畫狀態的外觀、轉移的規則,清楚地使用程式碼宣告在Action與Reducer,也就是說,哪些操作會導致哪些狀態,開發者都必須要有事先的預期。

當然,程式畢竟是依照開發者寫的方式來執行,不是按照著開發者所預期的來執行,因此,在遵守Redux的架構設計下,Reducer等元件會易於測試,而且,有了事先的規畫,以及事後的測試作為助力,狀態的變化才會符合預期!

作者簡介


Advertisement

更多 iThome相關內容