深入淺出React+Redux(三:Flux單向資料流,相關程式碼在github flux分支)
前言
通過上章,我們能感覺到僅僅通過prop和state 管理React大型專案,簡直是個巨大,恐怖乃至不可完成的挑戰。因為社群和個人喜愛還是推薦Redux做專案的狀態管理。但是作為單向資料流鼻祖的Flux,也是讀者需要整理下區別的。
(一)前端MVC 框架的缺陷
MVC框架是業界廣泛接受的一種前端應用架構型別,它把應用分成三個部分:
- Model (模型)負責管理資料 ,大部分業務邏輯也應該放在 Model 中;
- View (檢視)負責渲染使用者介面,應該避免在 View 中涉及業務邏輯;
- Controller (控制器)負責接受使用者輸入 根據使用者輸入呼叫對應的 Model 部分邏輯,把產生的資料結果交給 View 部分,讓 View 渲染出必要的輸出 。
MVC幾個部分組成部分和請求的關係圖
這種將一個應用劃分為多個元件,就是“分而治之”。但是現實專案足夠大後,實際情況是什麼樣子的呢。如下圖
實際上。這是客戶端區分伺服器的地方,在伺服器mvc依舊是霸主地位,它的一個完整請求是以Controller中的request發起,response結束(其實本身資料流也類似單向)·。
但是客戶端,總是允許View 和Model 可以直接通訊
。就會造成上面圖中的情況。
(二)FlUX基本概念
Facebook使用 Flux 框架來代替原有的 MVC 框架,他們提出的 Flux 框架大致結構如下圖。
首先,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
新的 Action
。
(三)FlUX 簡單demo
安裝依賴
$ yarn add flux --dev
(1)Dispatcher
首先,我們要創造一個 Dispatcher,
在src/appDispatcher/index.js
。創造這個唯一 的Dispatcher 物件
/**
* @component appDispatcher
* @description 全域性Dispatcher
* @time 2018/1/16
* @author jokerXu
*/
import {Dispatcher} from 'flux';
export default new Dispatcher();
Dispatcher 存在的作用,就是用來派發 action ,接下來我們就來定義應用中涉及的 action。
(2)Action
action 顧名思義代表一個“動作”,不過這個動作只是一個普通的 JavaScript 物件,代表一個動作的純資料
,類似於 DOM API 中的事件( event ) 。 甚至,和事件相比, action其實還是更加純粹的資料物件,因為事件往往還包含一些方法,比如點選事件就有
preventDefault 方法,但是 action 物件不自帶方法,就是純粹的資料 。
作為管理, action 物件必須有一個名為 type 的欄位,代表這個 action 物件的型別,
為了記錄日誌和 debug 方便,這個 type 應該是字串型別 。
定義 action 通常需要兩個檔案,一個定義 action 的型別,一個定義 action 的構造函
數(也稱為 action creator ) 。 分成兩個檔案的主要原因是在 Store 中會根據 action 型別做
不同操作,也就有單獨導人 action 型別的需要 。
首先在src/actionTypes/demo.js
中定義型別。
/**
* @component actionTypes
* @description demo動作型別
* @time 2018/1/22
* @author ***
*/
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
上面程式碼表示,執行兩次操作,一個事點選”+”,一個事點選”-“。
然後在src/actions/demo.js
中定義動作
/**
* @component actions
* @description demo動作
* @time 2018/1/22
* @author ***
*/
import * as ActionTypes from '../actionTypes/demo';
import AppDispatcher from '../appDispatcher';
export const increment = (counterCaption) => {
AppDispatcher.dispatch({
type: ActionTypes.INCREMENT,
value: counterCaption
})
};
export const decrement = (counterCaption) => {
AppDispatcher.dispatch({
type: ActionTypes.DECREMENT,
value: counterCaption,
})
};
雖然出於業界習慣,這個檔案被命名為 Actions. ,但是要注意裡面定義的並不是
action 物件本身,而是能夠產生並派發 action 物件的函式 。
我們匯出了兩個 action 建構函式 increment 和 decrement,當這兩個函式被調
用的時候,創造了對應的 action 物件,並立即通過 AppDispatcher.dispatch 函式派發出去 。
(3)Store
一個 Store 也是一個物件,這個物件儲存應用狀態,同時還要接受 Dispatcher 派發的
動作,根據動作來決定是否要更新應用狀態 。
我們創造兩個 Store ,一個是為 Counter 元件服務的 CounterStore ,另 一個就是為總
數服務的 SummaryStore 。
(1)定義src/stores/counterStore.js
/**
* @component stores
* @description demo的數量store
* @time 2018/1/22
* @author jokerXu
*/
import * as ActionTypes from '../actionTypes/demo';
import {EventEmitter} from 'events';
const CHANGE_EVENT = 'changed';
const counterValues = {
'First': 0,
'Second': 10,
'Third': 20,
};
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(CHANGE_EVENT, callback);
}
});
export default CounterStore;
當 Store 的狀態發生變化的時候, 需要通知應用的其他部分做必要的響應 。 在我們
的應用中,做出響應的部分當然就是 View 部分,但是我們不應該硬編碼這種聯絡,應
該用訊息的方式建立 Store 和 View 的聯絡 。 這就是為什麼我們讓 CounterStore 擴充套件了
EventEmitter.prototype ,等於讓 CounterStore 成了 EventEmitter 物件, 一個 EventEmitter
例項物件支援下列相關函式 。
- emit 函式,可以廣播一個特定事件,第一個引數是字串型別的事件名稱 ;
- on 函式,可以增加一個掛在這個 EventEmitter 物件特定事件上的處理函式,第一個引數是字串型別的事件名稱,第二個引數是處理函式;
- removeListener 函式, 和 on 函式做的事情相反, 刪除掛在這個 EventEmitter 物件特定事件上的處理函式,和 on 函式一樣, 第一個引數是事件名稱 ,第二個引數是處理函式 。
對於 CounterStore 物件, emitChange 、 addChangeListener 和 removeChangeListener 函
數就是利用 EventEmitter 上述的三個函式完成對 CounterStore 狀態更新的廣播、新增監昕
函式和刪除監昕 函式等操作 。
CounterStore 函 數還提供一個 getCounterValues 函式,用於讓應用中其他模組可以讀
取當前的計數值,當前的計數值儲存在檔案模組級的變數 counterValues 中 。
接下來將 Store 只有註冊到 Dispatcher 例項上才能真正發揮作用
import AppDispatcher from '../appDispatcher';
CounterStore.dispatchToken = AppDispatcher.register((action) => {
if (action.type === ActionTypes.INCREMENT) {
counterValues[action.value]++;
CounterStore.emitChange();
} else if (action.type === ActionTypes.DECREMENT) {
counterValues[action.value]--;
CounterStore.emitChange();
}
});
這是最重要 的 一 個 步 驟, 要 把 CounterStore 注 冊到全域性唯 一 的 Dispatcher 上 去。Dispatcher 有一個函式叫做 register ,接受一個回撥函式作為引數 。 返回值是一個 token ,這個 token 可以用於 Store 之間的同步。
現在我們來仔細看看 register 接受的這個回撥函式引數,這是 Flux 流程中最核心的部分,當通過 register 函式把一個回撥函式註冊到 Dispatcher 之後 , 所有派發給 Dispatcher的 action 物件 ,都會傳遞到這個回撥函式中來 。
比如通過 Dispatcher 派發一個動作,程式碼如下:
AppDispatcher.dispatch ({
type: ActionTypes.INCREMENT,
counterCaption: 'First '
});
根據不同的 type ,會有不同的操作,所以註冊的回撥函式很自然有一個模式,就是
函式體是一串 if-else 條件語句或者 switch 條件語句,而條件語句的跳轉條件,都是針對
引數 action 物件的 type 欄位
。
(2)定義src/stores/summaryStore.js
SummaryStore 也有 emitChange 、 addChangeListener 還有 removeChangeListener 函式,
功能一樣也是用於通知監昕者狀態變化,這幾個函式的程式碼和 CounterStore 中完全重複,
不同點是對獲取狀態函式的定義,
/**
* @component stores
* @description demo的總數store
* @time 2018/1/22
* @author jokerXu
*/
import CounterStore from './counterStore';
import * as ActionTypes from '../actionTypes/demo';
import {EventEmitter} from 'events';
const CHANGE_EVENT = 'changed';
function computeSumrnary(counterValues) {
let summary = 0;
for (const key in counterValues) {
if (counterValues.hasOwnProperty(key)) {
summary += counterValues[key];
}
}
return summary;
}
const SumrnaryStore = Object.assign({}, EventEmitter.prototype, {
getSummary: function () {
return computeSumrnary(CounterStore.getCounterValues());
},
emitChange: function () {
this.emit(CHANGE_EVENT);
},
addChangeListener: function (callback) {
this.on(CHANGE_EVENT, callback)
},
removeChangeListener: function (callback) {
this.removeListener(CHANGE_EVENT, callback);
}
});
export default SumrnaryStore;
可以注意到, SummaryStore 並沒有儲存自己的狀態,當 getSummary 被呼叫時,它
是直接從 CounterStore 裡獲取狀態計算的 。
可見,雖然名為 Store ,但並不表示一個 Store 必須要儲存什麼東西, Store 只是提供
獲取資料的方法,而 Store 提供的資料完全可以另一個 Store 計算得來 。
SummaryStore 在 Dispatcher 上註冊的回撥函式也和 CounterStore 很不一樣,程式碼
如下:
import AppDispatcher from '../appDispatcher';
SumrnaryStore.dispatchToken = AppDispatcher.register((action) => {
if ((action.type === ActionTypes.INCREMENT) ||
(action.type === ActionTypes.DECREMENT)
) {
AppDispatcher.waitFor([CounterStore.dispatchToken]);
SumrnaryStore.emitChange();
}
});
這裡使用了 waitFor 函式,這個函式解決的是下面描述的問題。
即使 Flux 按照 register 呼叫的順序去呼叫各個回撥函式,我們也完全無法把握各個Store 哪個先裝載從而呼叫 register 函式 。 所以,可以認為 Dispatcher 呼叫回撥函式的順序完全是無法預期的,不要假設它會按照我們期望的順序逐個呼叫 。
Dispatcher 的 waitFor 可以接受一個數組作為引數
,陣列中每個元素都是一個 Dispatcherregister 函式的返回結果,也就所謂的 dispatchToken 。 這個waitFor 函式
告訴 Dispatcher,
當前的處理必須要暫停,直到 dispatchToken 代表的那些已註冊回撥函式執行結束才能繼續 。
注意
Dispatcher 的 register 函式,只提供了註冊一個回撥函式的功能,但卻不能讓呼叫者在 register 時選擇只監聽某些 action,換句話說,每個 register 的呼叫者只能這樣請求:“ 當有任何動作被派發時,請呼叫我 。 ”但不能夠這麼請求:“當這種型別還有那種型別的動作被派發的時候,請呼叫我 。 ”
當一個動作被派發的時候, Dispatcher 就是簡單地把所有註冊的回撥函式全都呼叫一
遍,至於這個動作是不是對方關心的, Flux 的 Dispatcher 不關心,要求每個回撥函式去
鑑別 。
看起來,這似乎是一種浪費,但是這個設計讓 Flux 的 Dispatcher 邏輯最簡單化,
Dispatcher 的責任越簡單,就越不會出現問題 。 畢竟,由回撥函式全權決定如何處理 action
物件,也是非常合理的 。
(4)Views
存在於 Flux 框架中的 React 元件需要實現以下幾個功能:
- 建立時要讀取 Store 上狀態來初始化元件內部狀態;
- 當 Store 上狀態發生變化時,元件要立刻同步更新內部狀態保持一致;
- View 如果要改變 Store 狀態,必須而且只能派發 action 。
新增src/views/ControlPanel/Summary.js
/**
* @component Summary
* @description
* @time 2018/1/22
* @author ***
*/
import React, { Component } from 'react';
import SummaryStore from '../../stores/summaryStore.jsx';
class Summary 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>
);
}
}
export default Summary;
只要 CounterStore 發生變化, Counter 元件的 onChange 函式就會被呼叫。與 componentDidMount 函式中監昕事件相對應,在componentWillUnmount 函式中刪除了這個監昕 。
修改src/views/ControlPanel/index.js
import React, { Component } from 'react';
import Counter from './Counter';
import Summary from './Summary';
class ControlPanel extends Component {
constructor (props) {
super(props);
this.initValues = [
{
title: 'First',
},{
title: 'Second',
},{
title: 'Third',
}
];
}
// 遍歷子元件
mapCounter=() =>{
return this.initValues.map((value,index)=>{
return <Counter key={index} caption={value.title} />;
})
};
render() {
return (
<div>
{this.mapCounter()}
<hr/>
<Summary />
</div>
)
}
}
export default ControlPanel;
修改src/views/ControlPanel/Counter.jsx
/**
* @component Counter
* @description 子元件
* @time 2018/1/15
* @author ***
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import CounterStore from '../../stores/counterStore';
import * as Actions from '../../actions/demo';
class Counter extends Component{
constructor(props){
super(props);
this.state= {
count: CounterStore.getCounterValues() [props.caption],
}
}
clickIncrementHandler=() =>{
Actions.increment(this.props.caption);
};
clickDecrementHandler=() =>{
Actions.decrement(this.props.caption);
};
componentDidMount( ) {
CounterStore.addChangeListener(this.onChange) ;
}
componentWillUnmount() {
CounterStore.removeChangeListener(this.onChange);
}
onChange = () => {
const newCount = CounterStore.getCounterValues()[this.props.caption];
this.setState({count: newCount});
}
shouldComponentUpdate(nextProps , nextState) {
return (nextProps.caption !== this.props.caption) || (nextState.count !==this.state.count);
}
render(){
const buttonStyle= {
marginRight: '15px',
};
const { caption }= this.props;
const { count }= this.state;
return (
<div>
<button style={buttonStyle} onClick={this.clickIncrementHandler}>+</button>
<button style={buttonStyle} onClick={this.clickDecrementHandler}>-</button>
<span>{caption} count: {count}</span>
</div>
);
}
}
Counter.propTypes= {
caption: PropTypes.string.isRequired,
};
export default Counter;
(四)Flux 的優勢
回顧一下完全只使用 React 實現的版本,應用的狀態資料只存在於 React 元件之中,每個元件都要維護驅動自己渲染的狀態資料,單個元件的狀態還好維護,但是如果多個元件之間的狀態有關聯,那就麻煩了 。 比如 Counter 元件和 Summary 元件, Summary 元件需要維護所有 Counter 元件計數值的總和, Counter 元件和 Summary 分別維護自己的狀態,如何同步 Summary 和 Counter 狀態就成了問題, React 只提供了 props 方法讓元件之間通訊,元件之間關係稍微複雜一點,這種方式就顯得非常笨拙 。
Flux 的架構下,應用的狀態被放在了 Store 中, React 元件只是扮演 View 的作用,
被動根據 Store 的狀態來渲染 。 在上面的例子中, React 元件依然有自己的狀態,但是已
經完全淪為 Store 元件的一個對映,而不是主動變化的資料 。
Flux 帶來了哪些好處呢?最重要的就是“單向資料流”
的管理方式 。
在 Flux 的理念裡,如果要改變介面,必須改變 Store 中的狀態,如果要改變 Store 中
的狀態,必須派發一個 action 物件,這就是規矩 。 在這個規矩之下,想要追溯一個應用
的邏輯就變得非常容易 。
我們已經討論過 MVC 框架的缺點 MVC 最大的問題就是無法禁絕 View 和 Model 之
間的直接對話,對應於 MVC 中 View 就是 Flux 中的 View ,對應於 MVC 中的 Model 的就
是 Flux 中的 Store ,在 Flux 中, Store 只有 get 方法,沒有 set 方法,根本可能直接去修改
其內部狀態, View 只能通過 get 方法獲取 Store 的狀態,無法直接去修改狀態,如果 View
想要修改 Store 狀態的話,只有派發一個 action 物件給 Dispatcher。這看起來是一個“限制”,但卻是一個很好的“限制”,禁絕了資料流淚亂的可能 。簡單說來,在 Flux 的體系下,驅動介面改變始於一個動作的派發,別無他法 。
(四)Flux 的不足
(1). Store 之間依賴關係
在 Flux 的體系中,如果兩個 Store 之間有邏輯依賴關係,就必須用上 Dispatcher 的
waitFor 函式 。 而 dispatchToken 的產生,當然是 CounterStore 控制的,換句話說,要這樣設計:
- CounterStore 必須要把註冊回撥函式時產生的 dispatchToken 公之於眾;
- SummaryStore 必須要在程式碼裡建立對 CounterStore 的 dispatchToken 的依賴 。
雖然 Flux 這個設計的確解決了 Store 之間的依賴關係,但是,這樣明顯的模組之間
的依賴,看著還是讓人感覺不大舒服,畢竟,最好的依賴管理是根本不讓依賴產生 。
(2). 難以進行伺服器端渲染
在 Flux 的體系中,有一個全域性的 Dispatcher ,然後每一個 Store 都是一個全域性唯一
的物件,這對於瀏覽器端應用完全沒有問題,但是如果放在伺服器端,就會有大問題。
和一個瀏覽器網頁只服務於一個使用者不同,在伺服器端要同時接受很多使用者的請求,
如果每個 Store 都是全域性唯一的物件,那不同請求的狀態肯定就亂套了 。
並不是說 Flux 不能做伺服器端渲染,只是說讓 Flux 做伺服器端渲染很困難,實際
上, Facebook 也說的很清楚, Flux 不是設計用作伺服器端渲染的,他們也從來沒有嘗試
過把 Flux 應用於伺服器端。
(3). Store 混雜了邏輯和狀態
Store 封裝了資料和處理資料的邏輯,用面向物件的思維來看,這是一件好事,畢
竟物件就是這樣定義的 。 但是,當我們需要動態替換一個 Store 的邏輯時,只能把這個
Store 整體替換掉,那也就無法保持 Store 中儲存的狀態 。
最後
我們把 Flux 看作一個框架理念的話, Redux 是 Flux 的一種實現,除了 Redux 之外,
還有很多實現 Flux 的框架,比如 Reflux, Fluxible 等,毫無疑問 Redux 獲得的關注最多,
這不是偶然的,因為 Redux 有很多其他框架無法比擬的優勢 。