Flux - 為React打造的單向資料流架構


這篇文章中沒有半行程式碼,也不需要解說程式碼,只是講一些Flux架構的概念與技術重點。Flux的架構基本上是基於React特性設計而來的一種解決方案,它是React應用要進行規模化的必經之路。這篇文章希望提供一些個人的心得見解,供作網友們參考。

不論是Flux或其他以Flux架構為基礎延伸發展的函式庫(Alt、Reflux、Redux...)都是為了要解決同一個問題,這個問題在React應用進行規模化時會非常明顯,簡單的說就是:

應用程式領域(app domain)的狀態 - 簡稱為app state

應用程式都需要有app state(應用程式狀態),不論是在一個需要使用者登入的應用,要有全域的記錄著使用者登入的狀態,或是在應用程式中不同元件的溝通,都需要用到它。

React應用為什麼會有這個問題?原因其實是React元件的設計限制造成的,React本身只是個MVC架構中的View(視圖)函式庫,沒什麼架構或樣式可以作類似Model或Controller的事情,而且資料都包在元件,也就是在View(視圖)之中,還有以下的幾個問題。

有學過React的一些基礎的開發者應該會知道,元件無法直接修改state值,要透過setState來修改,這是其中一點。另外,在元件中的樹狀結構中,父元件(擁有者)與子元件(被擁有者)的關係,子元件是只能由父元件以props來傳遞屬性值,子元件本身無法更改props,這也是為什麼你一開始在學習React時,都會看到範例只有在最上層的元件有state值,而且都是由它來進行當有資料改變時的重新渲染工作,子元件通常只有負責呈現資料。

當然,有一個很技巧性的作法,是把父元件中的一個函式(方法)由props傳遞給子元件,然後在子元件觸發事件時,呼叫這個父元件的函式(方法),以此來達到子元件與父元件之間的溝通,間接更動父元件中的state。不過,這個作法並不直覺,而且你需要事先寫好與規範好兩邊的方法,當然在簡單的結構中,這溝通方式還可行,但如果是在複雜的元件結構時,或是有很多層或不同樹結構中的子元件要互相溝通時,這個作法根本沒什麼用處。

在複雜的元件樹狀結構時,唯一能作的方式,就是要將整個應用程式的變動資料整合在一起,然後獨立出來,也就是整個應用程式的Model(模型)。另外還需要對於資料的任何更動方式,也要獨立出來。這兩者組合在一起,就是稱之為應用程式領域的狀態,不過為了區分元件中的狀態(state),這個作為應用程式領域的持久性資料集合,會被稱為store(儲存)。

store(儲存)的角色並非只是元件中的state(狀態)而已,它也不會只有單純的記錄資料,可能在現今的每種不同的Flux延伸的函式庫,對於store的定義與設計都有所不同。在Flux的架構中的store中,它包含了對資料更動的函式,Flux稱這些函式為Store Queries(儲存查詢),也把它的角色定位為類似傳統MVC的Model(模型),但與傳統的Model(模型)明顯不同的是,store只能透過Action(動作)以"間接"的方式來自我更新(self-updates through Actions)。

store可以解決應用程式的狀態存放的問題,但它並非整個解決上面問題的核心,只能算是跨出第一步而已。困難的地方在於要如何在觸發動作時,進行儲存查詢,然後還要進行呈現資料的修改(剛說的由父元件用props設定到子元件),以及作整個應用程式的渲染,也就是整個流程的運作。

這一連串的步驟會匯整為一個資料流(Data Flow),Flux的名稱來由其實就是拉丁文中的Flow,Flux用單向(unidirectional)資料流來設計整個資料流的運作,也就是說整個資料的流動方向都是一致的,從在網頁上呈現的元件,被觸發事件後,傳送動作到發送器,再到store,最後進行整個應用的重新渲染,都是往一個方向執行。

下面是個簡單的示意圖,上面有標出主要的參與成員,來自Flux官網:

flux diagram

這個資料流的核心技術是一個稱為AppDispatcher(應用發送器)的設計,你可以把它想成是個發送中心,不論來自元件何處的動作,每個store在AppDispatcher上註冊,提供一個callback,這個callback是當有動作(action)產生時,會被呼叫使用的。

由於每個Action(動作)只是一個單純的物件,包含actionType與資料(通常稱為payload),我們會另外需要Action Creator(動作建立器),它們是一些輔助方法,除了建立動作外也會把動作傳給Dispatcher(發送器),也就是呼叫Dispatcher(發送器)的dispatch方法。

註: Payload用在電腦科學的意思,是指在資料傳輸時的"有效資料"部份,也就是不包含傳輸時的頭部資訊或metadata等等用於傳輸其他資料。它的英文原本是指是飛彈或火箭的搭載的真正有效的負載部份,例如炸藥或核子彈頭,另外的不屬於payload的部份當然就是火箭傳送時用的燃料或控制零件。

Dispatcher的用途就是把接收到的actionType與資料(payload),廣播給所有註冊的callbacks。它這個設計並非是獨創的,這在設計模式中類似於pub-sub(發佈-訂閱)系統,Dispatcher是類似Eventbus的概念,但在功能上有一些差異。

Dispatcher類別的設計很簡單,其中有兩個核心的方法,這兩個是互為相關的函式:

  • dispatch 發送payload(相當於動作)給所有註冊的callbacks。元件觸發事件時用這個方式來發送動作。
  • register 註冊在所有payload(相當於動作)發送時要呼叫的callbacks。這些callbacks就是上面說的會用來更動store的Store Queries(儲存查詢)。

在資料流的最後重點是,store要如何觸發最上層元件的setState,然後進行整體的重新渲染(re-render)工作。Flux提出的方式是把storeEventEmitter.prototype物件進行擴充,讓store具有監聽事件的能力,然後在最上層元件中的生命週期中,加入變動時的監聽事件,這是最後重新渲染的技術重點部份。

註: JavaScript內建的Event、CustomEvent等介面,以及addListener、dispatch等方法,只能實作在具有事件介面的網頁DOM元素上。單純在JavaScript的物件上是沒有辦法使用,要靠額外的函式庫才能這樣作。這是一定要使用類似像EventEmitter這種函式庫的主要原因。

不過,你可能會好奇,為什麼不乾脆一點直接在store上面作更動就好了,一定要拐這麼大一個彎,透過動作來用Action(動作)"間接"的方式來自我更新?

我想原因之一,是要標準化Action(動作)的規格,也就是所有在應用程式中的元件,都得要按照這些動作來觸發事件,發送器中註冊的callback也是要寫成處理同一種規格的動作。Action(動作)主要由type(類型)與payload(有效資料)組成,Flux Standard Action(Flux標準動作)就是提出來要標準化Action(動作)的格式,有了統一格式的Action物件,在更新資料時,更新的方式會更具統一性,這樣Flux才有辦法把整個資料流繼續運作下去。就像網路中的這些傳輸協定一樣,資料的格式與運作的流程,都有標準的規範,不是隨隨便便就可以進行傳輸。

當然不論是將Action(動作)標準化,或是只有單向的資料流,要這樣作的其中一個主要理由是要避免Event Chains(事件連鎖)的發生,簡單化與組織化整個資料從發送到更新,一直到應用程式的渲染工作,也就是像下面這樣的資料運作流程:

事件觸發 -> 由Action Creator呼叫Dispatcher.dispatch(action) -> Dispatcher呼叫已註冊的callback -> 呼叫對應的Store Queries(儲存查詢) -> 觸發Store更動事件 -> 進行整個應用的重新渲染

下面是最接近整個流程的一張圖,來自Flux: Actions and the Dispatcher:

此外,Flux中支援多個store,以及在Dispatcher中提供waitFor函式,藉此可以控制不同store更新順序,這是另一個為了規模化的設計。

Flux Utils(工具組)

值的一提的還有Flux Utils(工具組)裡面的東西,這是函式庫裡面的一些工具類別,並不是一開始Flux就有的,而是在2015.8月的2.1.0版本才加入的。這些是由Flux架構為基礎額外寫出來的,提供使用Flux架構的應用程式簡便的解決方案。你如果有看過,就會知道有很多後來的Flux延伸函式庫,多多少少都有參考這個工具組裡面的作法。我把它整理列出來在下面:

  • ReduceStore: 帶有reduce方法的store,繼承自ReduceStore的store不需要對reduce方法中的更改手動地作emit(觸發),reduce方法需要是pure(純粹)而且無副作用。
  • MapStore: 繼承自ReduceStore的更進階類別,資料結構使用的是Immutable Map。
  • Containers: 用來當作最上層(最外層)的容器元件,主要的功能是用來獲取自store的資訊,以及對應(或連接)到它自己的state,沒有UI邏輯也沒有props。

其中的ReduceStore應該就是一開始Redux的設計,這也是為什麼現在在Redux中都是用reducer來作store的建立,只要使用的是pure(純粹)而且無副作用的reduce方法,就可免去上面章節中那個用EventEmitter來監聽store的機制,這是因為React元件本身的重新渲染機制,是根據diff演算法,也就是比對要進行更新的新狀態與前一個狀態間的差異,然後作最佳的重新渲染,所以需要傳入目前狀態與新狀態兩個值,才能決定是不是要作重新渲染,React中的shouldComponentUpdate(nextProps, nextState)方法,就是用類似的架構在運作,詳見這裡的說明

Containers(容器)這個元件的設計,在其他Flux延伸的函式庫中也有存在,不過通常會稱為Provider(提供者),裡面有用類似的概念設計。

至於MapStore因為資料結構整個要改為Immutable Map,這是由Facebook另一個Immutable.js提供的,更嚴格的一種資料結構,Immutable是"不能改變的"意思,這部份的內容可以參考這裡的說明

會寫出本章節Flux Utils(工具組)的原因,是因為有很多人在學習像Redux函式庫時,會覺得它是另一個與Flux架構完全不同的東西,像reducer、pure function、Provider等等,實際是個天大的誤解,Redux的作者是把Flux工具組與React原本就有的作法與概念,發揚光大而且讓它更容易使用。

結語

以下為一些我對Flux的研究與使用感想:

  • 不容易學or用: 雖然它的概念與函式庫中的原始碼很少,但如果你要從頭到尾把架構理解清楚要花不少時間,除了一堆專有名詞與混雜的各種技術外,範例的程式碼也不容易理解。對初學React的開發者來說,學習曲線很高。
  • 拼裝車: 這架構感覺像是七拼八湊組合出來的,看範例中的程式碼零零碎碎的很多,有些地方還有外部函式、類別、方法混在一起,重覆使用的程式碼也不少,看起來似乎就是個概念性的實驗範例而已。

所以我不建議你直接使用所謂的Flux,也就是這套由Facebook一開始提出來的概念性架構,Flux因為是React的最早的創作小組所提出,針對React本質上的問題,以及解決的大方向都有提出來。但Flux自從發表後,整個主架構與原始碼都沒什麼太大變化,沒什麼太大的更新,所以並不建議你去使用它。你需要從Flux中學習的是,它是怎麼運作與要解決哪些問題,以及其中的幾個關鍵技術重點。在Flux之後有很多由這個架構延伸出來的函式庫,大部份還是依照整個流程與解決方向,但實作細節與額外的特色則是各家不同,用這些概念與技術重點來學習其他的延伸函式庫,會讓你的開發日子輕鬆些。如果你真的有空的話,Flux官網中有幾則相關影片,主要都是在談這個架構的設計概念,你如果有空可以看看。

你應該學其他延伸出來的函式庫,例如Redux等等,Redux現在的生態圈發展得很好,學習資源與使用者非常多,當你遇到問題時會容易找到解答,也有很多週邊的擴充或應用可以使用,所以Redux應該是首選。最近有另一套MobX,它是個新的函式庫,使用上更簡單直覺,不像Redux有很多入門基礎或是必定要遵守的規則,也很適合React入門的開發者使用,但它還是個新的專案,可能資源就沒那麼齊全,要使用哪一套函式庫就看你自己的選擇,都有不同的特色。

參考