從flux到redux
- 2019 年 12 月 5 日
- 筆記
從flux到redux
flux既是一個前端架構,更是一種狀態管理的思想。
2013年,Facebook公司讓React亮相的同時,也推出了Flux框架,React和Flux相輔相成,Facebook認為兩者結合在一起才能構建大型的JavaScript應用。
MVC的缺陷
談起MVC,我們想到的傳統MVC架構可能是這樣的:

但是,Facebook的工程部門在應用MVC架構時逐漸發現,對於非常巨大的程式碼庫和龐大的組織,「MVC真的很快就變得非常複雜」。每當工程師想要增加一個新的功能時,對程式碼的修改很容易引入新的bug,因為不同模組之間的依賴關係讓系統變得「脆弱而且不可預測」。對於剛剛加入團隊的新手,更是舉步維艱。

Facebook工程師眼中MVC的畫風
傳統的MVC框架,為了讓數據流可控,Controller應該是中心,當View要傳遞消息給Model時,應該調用Controller的方法,同樣,當Model要更新View時,也應該通過Controller引發新的渲染。
而瀏覽器端MVC框架,存在用戶的交互處理,介面渲染出來之後,Model和View依然存在於瀏覽器中,這時候就會誘惑開發者為了簡便,讓現存的Model和View直接對話。
所以Facebook提出了他們眼中的解決方案,那就是flux。

•Dispatcher,處理動作分發,維持Store之間的依賴關係;•Store,負責存儲數據和處理數據相關邏輯;•Action,驅動Dispatcher的JavaScript對象;•View,視圖部分,負責顯示用戶介面。
如果非要把Flux和MVC做一個結構對比,那麼,Flux的Dispatcher相當於MVC的Controller,Flux的Store相當於MVC的Model,Flux的View當然就對應MVC的View了,至於多出來的這個Action,可以理解為對應給MVC框架的用戶請求。
在MVC框架中,系統能夠提供什麼樣的服務,通過Controller暴露函數來實現。每增加一個功能,Controller往往就要增加一個函數;在Flux的世界裡,新增加功能並不需要Dispatcher增加新的函數,實際上,Dispatcher自始至終只需要暴露一個函數Dispatch,當需要增加新的功能時,要做的是增加一種新的Action類型,Dispatcher的對外介面並不用改變。當需要擴充應用所能處理的「請求」時,MVC方法就需要增加新的Controller,而對於Flux則只是增加新的Action。
在react中使用flux
現在用flux重構上篇文章創造的計數器。
npm i flux -S
頁面有三個參與計算的clickCounter組件,大致的思路是:每個組件都根據label獲取對應的狀態。
Dispatch
在src下創建Dispatcher:
// src/Dispatcher/index.js import {Dispatcher} from 'flux'; export default new Dispatcher();
在這裡,我們引入了dispatch類,然後創造一個新的對象作為這個文件的默認輸出就足夠了。在其他程式碼中,將會引用這個全局唯一的Dispatcher對象。
Dispatcher存在的作用,就是用來派發action,接下來我們就來定義應用中涉及的action。
Action
action顧名思義代表一個「動作」,不過這個動作只是一個普通的JavaScript對象,代表一個動作的純數據,類似於DOM API中的事件(event)。甚至,和事件相比,action其實還是更加純粹的數據對象,因為事件往往還包含一些方法,比如點擊事件就有preventDefault方法,但是action對象不自帶方法,就是純粹的數據。
作為管理,action對象必須有一個名為type的欄位,代表這個action對象的類型,為了記錄日誌和debug方便,這個type應該是字元串類型。定義action通常需要兩個文件:
•第一個文件(src/Action/ActionTypes.js)定義action的類型:type:
export default { INCREMENT:'increment', DECREMENT:'decrement' }
•第二個文件(src/Action/index.js)放構造函數,又名ActionCreator,它通過ActionTypes
對象向store提交"事件"請求,這些事件名就是actionTypes內定義的名稱:
import ActionTypes from './ActionTypes'; import Dispatcher from '../Dispatcher' const increment=(label)=>{ Dispatcher.dispatch({ type:ActionTypes.INCREMENT, label }) } const decrement=(label)=>{ Dispatcher.dispatch({ type:ActionTypes.DECREMENT, label }) } export default { increment, decrement }
這個index.js導出了兩個action構造函數increment和decrement,當這兩個函數被調用的時候,創造了對應的action對象,並立即通過AppDispatcher.dispatch函數派發出去。
store
一個Store也是一個對象,這個對象存儲應用狀態,同時還要接受Dispatcher派發的動作,根據動作來決定是否要更新應用狀態。同時,它還可以被用來獲取狀態,並不一定非要儲存狀態。
counterStore
首先關注子組件計數器的狀態,在src/store/CounterStore.js新建:
import Dispatcher from '../Dispatcher'; import ActionTypes from '../Action/ActionTypes'; import { EventEmitter } from 'events'; const CHANGE_EVENT = 'changed'; // 初始狀態 const counterValues = { firstCount: 0, secoundCount: 0, thirdCount: 0 } const CounterStore = Object.assign({}, EventEmitter.prototype, { // 獲取整個狀態 getCounterValues: function () { return counterValues; }, // 觸發事件 emitChange: function () { this.emit(CHANGE_EVENT); }, addChangeListener: function (callback) { this.on(CHANGE_EVENT, callback) }, removeChangeListener: function (callback) { this.removeListener(callback) } }); CounterStore.dispatchToken = Dispatcher.register((action) => { if (action.type === ActionTypes.INCREMENT) { // 更新引用對象,再觸發changed事件 counterValues[action.label]++; CounterStore.emitChange(); } else if (action.type === ActionTypes.DECREMENT) { counterValues[action.label]--; CounterStore.emitChange(); } }); export default CounterStore;
上述的程式碼中,我們讓CounterStore擴展了EventEmitter.prototype
,等於讓CounterStore成了EventEmitter對象,一個EventEmitter實例對象支援下列相關函數。
•emit函數,可以廣播一個特定事件,第一個參數是字元串類型的事件名稱;•on函數,可以增加一個掛在這個EventEmitter對象特定事件上的處理函數,第一個參數是字元串類型的事件名稱,第二個參數是處理函數;•removeListener函數,和on函數做的事情相反,刪除掛在這個EventEmitter對象特定事件上的處理函數,和on函數一樣,第一個參數是事件名稱,第二個參數是處理函數。要注意,如果要調用removeListener函數,就一定要保留對處理函數的引用。
對於CounterStore對象,emitChange、addChangeListener和removeChangeListener函數就是利用EventEmitter上述的三個函數完成對CounterStore狀態更新的廣播、添加監聽函數和刪除監聽函數等操作。
註冊到dispatcher
目前實現的Store只有註冊到Dispatcher實例上才能生效。Dispatcher有一個函數叫做register,接受一個回調函數作為參數。返回值是一個token,這個token可以用於Store之間的同步。
// 註冊的回調函數包含了業務方法 CounterStore.dispatchToken = Dispatcher.register((action) => { if (action.type === ActionTypes.INCREMENT) { counterValues[action.counterCaption] ++; CounterStore.emitChange(); } else if (action.type === ActionTypes.DECREMENT) { counterValues[action.counterCaption] --; CounterStore.emitChange(); } });
現在我們來仔細看看register接受的這個回調函數參數,這是Flux流程中最核心的部分,當通過register函數把一個回調函數註冊到Dispatcher之後,所有派發給Dispatcher的給Dispatcher的action對象,都會傳遞到這個回調函數中來。
總和計數器(SummaryStore)
總和計數器和counterStore的內容差不多,不同的地方在於就是提供了getSummary方法,此外,需要等待(waiteFor)CounterStore計算完畢後,才計算總和。
import Dispatcher from '../Dispatcher'; import ActionTypes from '../Action/ActionTypes'; import CounterStore from './CounterStore.js'; import { EventEmitter } from 'events'; const CHANGE_EVENT = 'changed'; function computeSummary(counterValues) { let summary = 0; for (const key in counterValues) { if (counterValues.hasOwnProperty(key)) { summary += counterValues[key]; } } return summary; } const SummaryStore = Object.assign({}, EventEmitter.prototype, { getSummary: function () { return computeSummary(CounterStore.getCounterValues()); }, emitChange: function () { this.emit(CHANGE_EVENT); }, addChangeListener: function (callback) { this.on(CHANGE_EVENT, callback); }, removeChangeListener: function (callback) { this.removeListener(CHANGE_EVENT, callback); } }); SummaryStore.dispatchToken = Dispatcher.register((action) => { if ((action.type === ActionTypes.INCREMENT) || (action.type === ActionTypes.DECREMENT)) { Dispatcher.waitFor([CounterStore.dispatchToken]); SummaryStore.emitChange(); } }); export default SummaryStore;
可能你會好奇,目前CounterStore已經把所有的狀態都儲存了,為什麼還會有一個SummaryStore?
的確,SummaryStore並沒有存儲自己的狀態,當getSummary被調用時,它是直接從CounterStore里獲取狀態計算的。CounterStore提供了getCounterValues函數讓其他模組能夠獲得所有計數器的值,SummaryStore也提供了getSummary讓其他模組可以獲得所有計數器當前值的總和。不過,既然總可以通過CounterStore.getCounterValues函數獲取最新鮮的數據,SummaryStore似乎也就沒有必要把計數器當前值總和存儲到某個變數里。
事實上,可以看到SummaryStore並不像CounterStore一樣用一個變數counterValues存儲數據,SummaryStore不存儲數據,而是每次對getSummary的調用,都實時讀取CounterStore.getCounterValues,然後實時計算出總和返回給調用者。可見,雖然名為Store,但並不表示一個Store必須要存儲什麼東西,Store只是提供獲取數據的方法,而Store提供的數據完全可以另一個Store計算得來。
總和計數器關注點在於註冊方法用了一個waitFor:
SummaryStore.dispatchToken = AppDispatcher.register((action) => { if ((action.type === ActionTypes.INCREMENT) || (action.type === ActionTypes.DECREMENT)) { AppDispatcher.waitFor([CounterStore.dispatchToken]); SummaryStore.emitChange(); } });
這樣我們已經註冊了兩個store到Disparcher上,派發事件的回調函數的順序是怎樣的呢?
可以認為Dispatcher調用回調函數的順序完全是無法預期的,不要假設它會按照我們期望的順序逐個調用。
flux的Dispatcher的waitFor方法。在SummaryStore的回調函數中,之前在CounterStore中註冊回調函數時保存下來的dispatchToken終於派上了用場。Dispatcher的waitFor可以接受一個數組作為參數,數組中每個元素都是一個Dispatcher.gister函數的返回結果,也就所謂的dispatchToken。這個waitFor函數告訴Dispatcher,當前的處理必須要暫停,直到dispatchToken代表的那些已註冊回調函數執行結束才能繼續。
在源碼實現上:當一個派發動作發生後,Dispatcher會檢查weitFor中的狀態回調函數是否被執行了,只有被執行了,才會根據新的狀態來計算。因此,即使SummaryStore比CounterStore提前接收到了action對象,在emitChange中調用waitFor,也就能夠保證在emitChange函數被調用的時候,CounterStore也已經處理過這個action對象,一切完美解決。
這裡要注意一個事實,Dispatcher的register函數,只提供了註冊一個回調函數的功能,但卻不能讓調用者在register時選擇只監聽某些action,換句話說,每個register的調用者只能這樣請求:「當有任何動作被派發時,請調用我。」但不能夠這麼請求:「當這種類型還有那種類型的動作被派發的時候,請調用我。」
當一個動作被派發的時候,Dispatcher就是簡單地把所有註冊的回調函數全都調用一遍,至於這個動作是不是對方關心的,Flux的Dispatcher不關心,要求每個回調函數去鑒別。
view
view並不是非得使用react,你可以使用任何喜歡的哪怕是自創的頁面框架。但無論是哪個架構,都少不了以下流程:
•創建時要讀取Store上狀態來初始化組件內部狀態;•當Store上狀態發生變化時,組件要立刻同步更新內部狀態保持一致;•View如果要改變Store狀態,必須而且只能派發action。
回到總體的頁面,我們不需要再把子組件的計算邏輯放到最上層來了。
import React, { Component } from 'react'; import ClickCounter from './ClickCounter' import AllCount from './AllCount'; const styles={ app:{ width:250, margin:'100px auto' } } class App extends Component { constructor(props) { super(props) } render() { return ( <div style={styles.app}> <ClickCounter label={'firstCount'} /> <ClickCounter label={'secoundCount'}/> <ClickCounter label={'thirdCount'} /> <hr /> <AllCount /> </div> ); } } export default App;
clickCounter組件,需要在不同生命周期更新:
import React, { Component } from 'react' import CounterStore from './stores/CounterStore'; import Actions from './Action' const styles = { counter: { width: 250, display: 'flex' }, showbox: { width: 80, textAlign: 'center' }, label: { width: 120, } } class ClickCounter extends Component { constructor(props) { super(props) 根據label從總體狀態中拿到屬於自己的`FIRST/SECOUND/THIRD`count this.state = { count: CounterStore.getCounterValues()[props.label] } } componentDidMount() { // 開啟監聽 CounterStore.addChangeListener(this.onChange); } componentWillUnmount() { // 防止記憶體泄漏 CounterStore.removeChangeListener(this.onChange); } onChange = () => { const newCount = CounterStore.getCounterValues()[this.props.label]; this.setState({ count: newCount }); } onClickIncrementButton = () => { Actions.increment(this.props.label); } onClickDecrementButton = () => { Actions.decrement(this.props.label); } render() { return ( <div style={styles.counter}> <div style={styles.label}>{this.props.label}</div> <button onClick={this.onClickDecrementButton}>-</button> <div style={styles.showbox}>{this.state.count}</div> <button onClick={this.onClickIncrementButton}>+</button> </div> ) } } export default ClickCounter;
在總和計數器上,就比較簡單了:
import React, { Component } from 'react' import SummaryStore from './stores/SummaryStore'; export default class extends Component { constructor(props) { super(props); this.state = { sum: SummaryStore.getSummary() } } componentDidMount() { // 監聽變化 SummaryStore.addChangeListener(this.onUpdate); } componentWillUnmount() { SummaryStore.removeChangeListener(this.onUpdate); } onUpdate = () => { this.setState({ sum: SummaryStore.getSummary() }) } render() { return ( <div>Total Count: {this.state.sum}</div> ); } }
flux的優與劣
和純react的state維護相比,flux架構下的計數器有了明顯變化。
回顧一下純React實現的版本,應用的狀態數據只存在於React組件之中,每個組件都要維護驅動自己渲染的狀態數據,單個組件的狀態還好維護,但是如果多個組件之間的狀態有關聯,那就麻煩了。
比如ClickCounter組件和AllCount組件,AllCount組件需要維護所有ClickCounter組件計數值的總和,ClickCounter組件和AllCount分別維護自己的狀態,如何同步AllCount和ClickCounter狀態就成了問題,React只提供了props方法讓組件之間通訊,組件之間關係稍微複雜一點,這種方式就顯得非常笨拙。
Flux的架構下,應用的狀態被放在了Store中,React組件只是扮演View的作用,被動根據Store的狀態來渲染。在上面的例子中,React組件依然有自己的狀態。但是已經淪落為store的一個映射,而不是主動變化的數據。
因此flux的優勢可以歸結為"單向數據流"。
在Flux的理念里,如果要改變介面,必須改變Store中的狀態,如果要改變Store中的狀態,必須派發一個action對象,這就是規矩。在這個規矩之下,想要追溯一個應用的邏輯就變得非常容易。
MVC最大的問題就是無法禁絕View和Model之間的直接對話,對應於MVC中View就是Flux中的View,對應於MVC中的Model的就是Flux中的Store,在Flux中,Store只有get方法,沒有set方法,根本不可能直接去修改其內部狀態,View只能通過get方法獲取Store的狀態,無法直接去修改狀態,如果View想要修改Store狀態的話,只有派發一個action對象給Dispatcher。
flux也存在一些缺點:
在Flux的體系中,如果兩個Store之間有邏輯依賴關係,就必須用上Dispatcher的waitFor函數。要通過waitFor函數告訴Dispatcher,先讓CounterStore處理這些action對象,只有CounterStore搞定之後SummaryStore才繼續。那麼,SummaryStore如何標識CounterStore呢?靠的是register函數的返回值dispatchToken,而dispatchToken的產生,當然是CounterStore控制的,換句話說,要這樣設計:
•CounterStore必須要把註冊回調函數時產生的dispatchToken公之於眾;•SummaryStore必須要在程式碼里建立對CounterStore的dispatchToken的依賴。
此外flux也難以在服務端進行渲染。