前端狀態管理框架之Redux

xiangzhihong發表於2018-07-17

隨著應用程式單頁面需求的越來越複雜,應用狀態的管理也變得越來越混亂。應用的狀態不僅包括從伺服器獲取的資料,還包括本地建立的資料,以及反應本地UI狀態的資料,而Redux正是為解決這一複雜問題而存在的。

用Redux官網的話來概括什麼是Redux:Redux是針對JavaScript應用的可預測狀態容器

這句話雖然簡短,但其實是有幾個涵義的:

  • 可預測的(predictable): 因為Redux用了reducer與純函式(pure function)的概念,每個新的state都會由舊的state建來一個全新的state,這樣可以作所謂的時光旅行除錯。因此,所有的狀態修改都是"可預測的"。
  • 狀態容器(state container): state是集中在單一個物件樹狀結構下的單一store,store即是應用程式領域(app domain)的狀態集合。
  • JavaScript應用: 這說明Redux並不是單指設計給React用的,它是獨立的一個函式庫,可通用於各種JavaScript應用。

有些人可能會認為Redux一開始就是Facebook所建立的專案,其實不然,它主要是由Dan Abramov所開始的一個專案,Dan Abramov進入Facebook的React核心小組工作是最近的事情。在此之前,他還有建立另外還有其他相關專案,像React Hot Loader、React DnD,可能比當時的Redux專案還更廣為人知,在Facebook發表Flux架構不久之後,許多Flux架構的類似函式庫/框架,不論是加強版、進化版、大改版等等非常的多。Redux一開始的對外演示的大型活動,是在2015年的React-Europe研討會,視訊Live React: Hot Reloading with Time Travel。在視訊中就有一個簡單的說明,Redux用了"Flux + Elm"的概念。

當然除了Flux與Elm之外,還有其他的主要像RxJS中的概念與設計方式,Redux融合了各家的技術於一身,除了更理想的使用在Flux要解決的問題上之外,更延伸了一些不同的設計方式。但是對初學者來說,它也不容易學習,網路上常常見到初學者報怨Redux實在有夠難學,這也並不是完全是Redux的問題,基本上來說Flux的架構原本就不是很容易理解,Redux還簡化了Flux的流程與開發方式。

所以我們要理解Redux是什麼,我們開始可以從這Flux與Elm兩大基礎來理解,以下分別說明一些基本的概念。

Flux

不論是Flux或其他以Flux架構為基礎延伸發展的函式庫(Alt、Reflux、Redux...)都是為了要解決同一個問題,這個問題在React應用規模化時會非常明顯,簡單以一句話來說就是:應用程式領域(app domain)的狀態 - 簡稱為App state

應用程式都需要有App state(應用程式狀態),不論是在一個需要使用者登入的應用,要有全域性的記錄著使用者登入的狀態,或是在應用程式中不同操作介面(元件)或各種功能上的資料溝通,都需要用到它。如果你已經有一些程式語言或應用的開發經驗,你應該知道這會像是MVC設計模式中的Model(模型)部份該作的事情。

React應用為什麼會出現這個問題?原因主要是來自React元件的本身設計造成的。React被設計為一個相似於MVC架構中的View(檢視)的函式庫,當然實際上它可以作的事情比MVC中的View(檢視)還要更多,但本質上的確React不是一個完整的應用程式開發框架,裡面沒有額外的架構可以作類似Model(模型)或Controller(控制器)的事情。對小型的元件或應用而言,應用的資料都包含在裡面,也就是在View(檢視)之中。

有學過React的一些基礎的開發者應該會知道,在React中的元件是無法直接更動state(狀態)的包含值,要透過setState方法來進行更動,這有很大的原因是為了Virtual DOM(虛擬DOM)的所設計,這是其中一點。另外在元件的樹狀階層結構,父元件(擁有者)與子元件(被擁有者)的關係上,子元件是隻能由父元件以props(屬性)來傳遞屬性值,子元件自己本身無法更改自己的props,這也是為什麼一開始在學習React時,都會看到大部份的例子只有在最上層的元件有state,而且都是由它來負責進行當資料改變時的重新渲染工作,子元件通常只有負責呈現資料。

當然,有一個很技巧性的方式,是把父元件中的方法宣告由props傳遞給子元件,然後在子元件觸發事件時,呼叫這個父元件的方法,以此來達到子元件對父元件的溝通,間接來更動父元件中的state。不過這個作法並不直覺,需要事先規範好兩邊的方法。在簡單的應用程式中,這溝通方式還可行,但如果是在有複雜的元件巢狀階層結構時,例如層級很多或是不同樹狀結構中的子元件要互相溝通時,這個作法是派不上用場的。

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

store(儲存)並不是只有應用程式單純的資料集合而已,它還包含了所有對資料的變更方法。

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

store的設計可以解決應用程式的狀態存放與更動的問題,但它還不能完整的解決整個問題,只是一個開端。最困難的地方在於,要如何在觸發動作時,進行store(儲存)的更動查詢,以及進行呈現資料的更動與最後作整個應用程式的渲染。這一連串的步驟,整合為一個資料流(Data Flow),Flux的名稱來由其實就是拉丁文中的Flow,Flux用單向(unidirectional)資料流來設計整個資料流的運作,也就是說整個資料的流動方向都是一致的,從在網頁上呈現的操作介面元件,被觸發事件後,傳送動作到傳送器,再到store,最後進行整個應用的重新渲染,都是往單一個方向執行。

所以說,單向資料流是Flux架構的核心設計。下面是Flux簡單的流程示意圖:

前端狀態管理框架之Redux

這個資料流的位於最中心的設計是一個AppDispatcher(應用傳送器),你可以把它想成是個傳送中心,不論來自元件何處的動作都需要經過它來傳送。每個store會在AppDispatcher上註冊它自己,提供一個callback(回撥),當有動作(action)發生時,AppDispatcher(應用傳送器)會用這個回撥函式通知store。

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

Dispatcher(傳送器)的用途就是把接收到的actionType與資料(payload),廣播給所有註冊的callbacks。它這個設計並非是獨創的,這在設計模式中類似於pub-sub(釋出-訂閱)系統,Dispatcher則是類似Eventbus的概念。Dispatcher類的設計很簡單,其中有兩個核心的方法,這兩個是互為相關的函式:

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

在資料流的最後,store要觸發最上層元件的setState,然後進行整體React的重新渲染工作。Flux提出的方式是一種自訂事件的監聽方式,把store用EventEmitter.prototype物件進行擴充,讓store具有監聽事件的能力,然後在最上層元件中的生命週期中,加入有更動時的監聽事件。這是由於JavaScript中內建的Event、CustomEvent等介面,以及addListener、dispatch等方法,只能實作在具有事件介面的網頁DOM元素上。單純在JavaScript的物件上是沒有辦法使用,要靠額外的函式庫才能這樣作,這是一定要使用類似像EventEmitter這種函式庫的主要原因。

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

我想原因之一,是要標準化Action(動作)的規格,也就是所有在應用程式中的元件,都得要按照這些動作來觸發事件,傳送器中註冊的callbacks(回撥)也是要寫成處理同一種規格的動作。Action(動作)主要由type(型別)與payload(有效資料)組成,Flux Standard Action(Flux標準動作)就是提出來要標準化Action(動作)的格式,有了統一格式的Action物件,在重新整理資料時所有重新整理方式會具統一性,這樣Flux才有辦法把整個資料流運作完成一個迴圈再接著下一個。就像網路的傳輸協定一樣,資料的格式與運作的流程,都有標準的規範,不是隨隨便便就可以進行傳輸。當然還有一些其它的原因,例如要避免Event Chains(事件連鎖)的發生。

其整個流程可以用下面的方式表示:

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

複製程式碼

總結來說,Flux使用了單向資料流的設計架構,是為了要解決React的應用程式領域狀態的問題。Flux的實作並不容易,有許多實作上的細節與開發步驟上都有分割不明確的問題,所以在此並不討論Flux的實作部份。在Flux發表之後(約為2014年中),陸陸續續出現了許多函式庫與框架,都是基於Flux的基本設計概念,都是為了要改善、簡化或自動化其中的實作步驟為主,而Redux也是其中一套。在經過一段時間之後,目前較熱門的與較多人使用的,就屬Redux,它有很多的設計概念都來自於Flux,能多加理解Flux的基本設計概念,對於學習Redux是絕對有幫助的。

Elm

或許你有聽過函式式程式開發(functional programming, FP)的開發風格,FP是什麼?用下面的一句話來說明,摘譯自這篇教程文件: 函式式程式開發就是隻使用"純粹函式"與"不可改變的值"來撰寫軟體應用的一種方式。

FP是現今相當熱門的一種程式開發風格,在很早之前就已經有一些純函式式程式開發的語言例如Haskell與OCaml,Elm也是一個純函式式程式開發的語言,它是一個很年輕的語言,Elm是專門用來開發網站應用程式的程式語言,最終編譯為JavaScript在網頁上執行,它與JavaScript語言有多差異很大的設計,例如:

  • Elm是強(靜態)資料型別的,它的資料型別也滿多樣的;
  • Elm是純FP的語言;
  • Elm-Architecture是包含在Elm的應用框架,它是單向資料流的架構。

React與Flux中有許多設計,都有應用到FP的設計,與Elm中一部份設計相當類似。而Redux又使用更多Elm中的設計,尤其是Elm-Architecture而來的,例如:

  • 不可改變性(Immutability): 所有的值在Elm中都是不可改變的,Redux中的純函式(pure function)與Reducer的設計很類似,React的設計中也有這類的概念
  • 時光旅行除錯(Time Traveling Debugger): 在Elm有這個設計,Redux學了過來

說明:Redux作者使用了FP(函式式程式開發)與Elm的架構,改進或簡化原本的Flux架構

Redux特性

Redux是目前最熱門的、最多人使用的Flux架構類的函式庫,雖然Redux也可以用於其他的函式庫,但基本上它是專門為了React應用所打造的。如果你真的要學會React,並用它來開發一個稍有規模的應用,學習Redux說是一條必經之路,當然也有其他的Flux架構類函式庫可以選擇,不同的函式庫有可能使用的解決方式與樣式相差會非常大。目前來說Redux的開發社群是最龐大也是最活躍的,而且不見得其他的函式庫就會更容易學習與使用,畢竟用得人多,你會遇到的問題大概都有人遇過,也都能找得到解決方式,這是開原始碼生態圈的紅利。

Redux會受歡迎不是沒有原因的,以下分析幾個Redux的優點:

1,使用了FP(函式式程式開發)與React可以配合得很好

Redux不同於Flux架構,它改採幾乎是純FP(函式式程式開發)的解決方式,目的是為了要簡化Flux中資料流的處理實作,也的確可以與React中的元件渲染配合得很好,這證明了它是找到了一個較為理想的與React應用能密切合作的解決方式。FP(函式式程式開發)也是目前JavaScript界的熱門主題,Redux也因此吸引到不少開發者的目光。

2,時光旅行除錯/熱重新載入

Redux一開始就附了時光旅行除錯工具與熱重新載入(hot reloading)的工具來提升開發體驗,這對開發者有很大的吸引力,這也代表在Redux應用上的資料變動,可以更容易的測試與除錯,這是其他Flux架構類函式庫或框架中所沒有的見到的。

3,更簡化的程式碼,更多可能的延伸應用

Redux一開始的版本只有99行程式碼,這可能比一開始的Flux架構使用的API更要少,不過程式碼少不見得概念就簡單,FP的撰寫風格多半追求的是更簡短的程式碼,這需要高超的技巧、深度的概念與不少的基礎。Redux一開始就可以很容易的使用於伺服器端渲染,而且也不限於使用於React應用上,這也吸引了更多的開發者使用意願。

4,更多的檔案,發展良好的生態圈

Redux作者一開始就撰寫非常多的檔案與教程,讓許多開發者能更快捷地掌握Redux的應用技術,Redux作者也是技術討論區的常客,常常可以看到他在討論區上回覆相關的問題。Redux的專案也是相當活躍的,有非常多的參與者在討論與解決問題,對於重大效能/臭蟲問題也是很快捷地解決。

相關文章