Flux 深度解讀(翻譯)

YaHuiLiang(Ryou)發表於2019-03-04

FLux 是 facebook 用於構建 Web 客戶端的一種應用架構。它利用單向資料流,來幫助複雜的 React 組合元件的狀態管理。它是一種模式,而不僅僅是一個框架,你可以不需要寫任何新程式碼來將 Flux 直接應用到你的應用當中。

基於 Flux 的應用程式需要包含三個主要部分:dispatcher,store 和 view(React元件)。這並不能與 MVC 模式混淆,C 確實存在於 Flux 架構中,但是它們是 controller-views -- 通常在最頂層的檢視需要將資料傳遞給它的子元件。另外,action 的生產者 dispatcher 相關方法用於描述應用中各種可能的狀態改變。這對於我們思考第四部分非常有幫助。

Flux 避開了 MVC,採取了單向資料流,當使用者與 React 檢視進行互動的時候,檢視通過 dispatcher 方法傳遞一個 action 物件到儲存資料和業務邏輯的各個儲存物件區 store 中。這些儲存區的資料變化會影響所有檢視,並導致檢視發生更新。這與 React 的程式設計風格有關,該風格允許通過資料的變化來改變檢視,而不需要指定如何通過狀態切換檢視。

我們最初的目的是能夠正確的通過驅動資料:比如,我們希望顯示訊息執行緒未讀計數,而另一個檢視顯示執行緒列表,高亮顯示未讀執行緒。這在 MVC 中很難處理 --- 將一個單一執行緒指派為讀更新執行緒,然後需要更新未讀計數。這些依賴和關聯經常出現在大型 MVC 應用中。資料流和不可預測的操作交織在一起。

控制和儲存相反:儲存接受更新並在適當的時候進行協調處理,而不是在一致的依賴外部更新資料的方式。儲存區外部的任何東西都無法觀察它內部的資料變化,這可以幫助我們保持清晰。儲存區中並沒有提供任何修改資料的方法,而是隻有一個簡單的途徑來將新資料推送到儲存區中 -- 註冊的 dispatcher。

結構和資料流

在 Flux 應用中,資料的流向是單一的:

Flux 深度解讀(翻譯)
單向資料流是 Flux 模式的核心,在上圖應該是__Flux 程式的主要模型__。dispatcher ,stores 和 views 是獨立的節點,具有不同的資料和輸出。actions 是簡單的物件,包括一個 type 標實屬性和新的資料。

檢視產生新的 action 在系統中傳播,以響應使用者的操作:

Flux 深度解讀(翻譯)
所有的資料流都要通過 dispatcher 來完成。action 在 action 生成器中被提供給 dispatcher,並且大部分來自於使用者的頁面互動操作。然後 dispatcher 執行在 store 上註冊的回撥,將 action 傳入到所有的 store 中。在這些註冊的回撥中,store 對 action 做對應的關聯處理。然後這些 store 通知一個變更事件,去告知 controller-views 資料發生了變化。controller-views 呼叫他們自己的 setState 方法,觸發一次重新渲染。更新整個相關的元件樹。
Flux 深度解讀(翻譯)
這種結構可以幫助我們很容易的預測程式的執行結果,這種方式讓我們聯想起了函數語言程式設計,或者資料流程式設計,基於資料的程式設計。在這些程式設計中,資料以單一的方向在程式中流動 -- 不存在雙向繫結。應用狀態僅存在 store 中,所以應用程式可以不同程度的進行解藕。在 store 間保持著依賴關係,通過 dispatcher 進行同步的更新管理。

我們發現,雙向資料繫結導致聯動更新,其中一個物件的改變就會導致另一個物件改變,也可能出發更多的更新。隨著程式的增長,這些聯動更新變得難以預測。一次使用者操作會帶來複雜的資料更新,導致難以預測。但是,如果只能單向的更新資料的話,這一切將變得更好預測。

讓我們自己看看 Flux 的各個組成部分。

一個單一的 Dispatcher

dispatcher 作為資料流管理程式中的派發器。它其實是一個被註冊到 store 中的一個回撥函式。並沒有自己的職能 -- 它將 action 傳入到 store 中。每一個 store 都需要註冊並提供一個回撥函式。每一個 action 都是通過 dispatcher 被傳入到 store 中的。

隨著應用的迭代,dispatcher 變得更加重要,它可以用於在 store 間通過呼叫回撥來按順序管理依賴關係。store 可以等待其他 store 完成更新,然後再更新自己。

Facebook 用於生產的 dispatcher 可以在npm, Bower, 和 GitHub中找到。

Stores

store 中包含應用的狀態和邏輯。它的角色有點類似 MVC 中的 M,但是它們管理一些物件狀態 -- 他們不像 ORM 那樣表示單個資料記錄。這和 Backbone 的集合不同。和管理單一的 ORM 風格的資料物件集合不同。store 管理特定域內的資料狀態。

例如:Facebook 的 Lookback 媒體編輯工具使用了一個叫做 Lookback Video Editor 的技術來跟蹤回放時間和回放狀態。在另一方面,同一個應用的映象儲存保持一個圖片的集合。在 TodoMVC 例子 這個例子的中, TodoStore 用於管理一個簡單的代辦事項集合。Store 即是一個資料集又是一個單模型域。

就像上面提到的,一個 store 會註冊一個 dispatcher 並且提供一個回撥函式。這個回撥函式接收 action 物件作為引數。對於 store 中註冊的回撥函式中,根據操作的型別提供一個 switch 操作來對 action 的 type 有針對性的進行處理。這允許操作通過 dispatcher 更新 store 中的資料狀態。之後,store 就進行了更新,他們通過廣播事件來通知它們的狀態發生了改變,因此檢視會查詢最新的狀態,然後更新自己。

Views and Controller-Views

React 提供了檢視層所需要的各種組合方式以及自由的檢視層渲染方式。在檢視層的最頂層,一種特殊的檢視用於監聽它所依賴的 store 中的廣播事件(註冊的回撥)。我們稱之為 controller-view(react-redux的connectWrapper),因為它們提供了從 store 中獲取資料的方式,並將這些資料傳遞給它的後代。我們也許需要一個 controller-view 來管理頁面的各個部分。

當它從 store 中接受到事件,它首先通過 store 提供的取資料方法拿到新的資料(mapStateToProps)。然後呼叫自己的 setState 或者是 forceUpdate 方法來讓 React元件及其子元件重新渲染。

我們經常將整個 store 狀態傳遞給單個物件的檢視鏈中,允許不同的子元件使用它們需要的東西。除了將控制器的行為保持在檢視結構的頂層,從而使我們的後代檢視在功能上儘可能的純粹之外,在單一物件中專遞整個狀態可以減少我們需要管理的數量。

有時候我們需要在檢視結構中新增更多的 controller-view,以保持元件的單一性。這有助於我們更好的封裝與特定資料相關聯的檢視的一部分。但是,請注意,在層次結構中更深的 controller-view 可能會為資料流引入新的,可能衝突的資料。在決定是否需要新增深度 controller-view 時,要平衡簡單元件和不同位置流引入更多資料更新的複雜度。這些多個資料更新可能會導致奇怪的效果,通過來自不同的 controller-view 更新反覆呼叫 React 的 render 方法,這可能會增加除錯難度(Redux通過單一資料來源很好的解決了這個問題)。

Actions

dispatcher 提供一個對外的方法,我們通過這個方法向 store 派發 action 物件。我們會將生成 action 物件的邏輯封裝成一個方法,該方法觸發 dispatcher 並將 action 傳入 dispatcher。例如:我們需要將代辦事項列表中的待辦事項內容。我們需要在 TodoActions 中建立一個函式 updateText(todoId, newText),在這個函式中生成我們需要的 action 物件,並將之繫結到 view 的一個互動事件上。當使用者觸發這個互動事件的時候,這個方法就被執行了,建立了 action 物件,並將其傳遞給 dispatcher,dispatcher 將 action 傳遞給 store,store 根據 action 中的 type 屬性進行資料處理。這個 type 屬性可以像這個樣子 TODO_UPDATE_TEXT。

Action 也可以從其它地方獲取,比如伺服器。例如在初始化階段,當伺服器返回錯誤程式碼,或者伺服器有資料更新的時候,就會發生這種情況。

關於Dispatcher?

像之前提到的那樣,dispatcher 也可以在相關的 store 之間進行分發。這個功能可以通過 Dispatcher 類中的 waitFor 來完成,我們不需要在一些簡單應用中這麼做。比如TodoMVC application,但是我們在更大更復雜的應用中,這麼做是非常有必要的。

TodoStore 中註冊了一個回撥,我們可以在這裡更新資料或者是做更進一步的處理:

case 'TODO_CREATE':
  Dispatcher.waitFor([
    PrependedTextStore.dispatchToken,
    YetAnotherStore.dispatchToken
  ]);

  TodoStore.create(PrependedTextStore.getText() + ' ' + action.text);
  break;
複製程式碼

waitFor 接受一個引數,該引數是一個包含多個 dispatcher 的陣列。因此,store 呼叫 waitFor 可以像處理自己的資料狀態一樣處理其它 store 中的資料狀態。

在為 Dispatcher 註冊回撥的時候,它會返回一個 dispatcher token。

PrependedTextStore.dispatchToken = Dispatcher.register(function (payload) {
  // ...
});
複製程式碼

更多有關 waitFor,actions,action creaters 和 dispatcher 的內容,請參考Flux: Actions and the Dispatcher

相關文章