React框架本身只應(yīng)用于View,如果基于MVC模式開發(fā),還需要Model和Control層,這樣催生了Flux的產(chǎn)生,而Redux是基于Flux理念的一種解決方式。
從《React入門系列》可知,組建直接傳遞參數(shù)或者事件都需要props一層層代理,對(duì)于復(fù)雜組件,它可能嵌套的子組件非常多,層級(jí)也比較深,那么,如果還采用props鏈條來維護(hù)組件通信或者數(shù)據(jù)共享,將非常困難,也不利于開發(fā)和維護(hù)。
Flux
Flux框架也是一種MVC框架,不同于傳統(tǒng)的MVC,它采用單向數(shù)據(jù)流,不允許Model和Control互相引用。Flux框架大致如下(來自網(wǎng)絡(luò)):
- Actions: 驅(qū)動(dòng)Dispatcher發(fā)起改變
- Dispatcher: 負(fù)責(zé)分發(fā)動(dòng)作(事件)
- Store: 儲(chǔ)存數(shù)據(jù),處理數(shù)據(jù)
- View: 視圖部分
Dispatcher只會(huì)暴露一個(gè)函數(shù)-dispatch,接受Action為參數(shù),發(fā)起動(dòng)作。如果需要增加新功能,不需要改變或者增加接口,只需增加Action類型。Dispatch的初始化和使用如下:
// Dispatcher.js
import {Dispatcher} from 'flux';
export default new Dispatcher();
// actions
import AppDispatcher from './Dispatcher.js';
export const increment = (number) => {
AppDispatcher.dispatch({
type: 'ADD',
value: number
});
};
Store 一般會(huì)繼承EventEmitter,實(shí)現(xiàn)事件監(jiān)聽,發(fā)布,卸載。需要將store注冊(cè)到Dispatcher實(shí)例上才能夠發(fā)揮作用。
Store可以直接修改對(duì)象,這點(diǎn)和Redux不同。
import AppDispatcher from './Dispatcher.js';
let value = 10;
const store = Object.assign({}, EventEmitter.prototype, {
getValue: function() {
return value;
}
emitChange: function() {
this.emit('change');
},
addChangeListener: function(callback) {
this.on('change', callback);
},
removeChangeListener: function(callback) {
this.removeListener('change', callback);
}
});
store.dispatchToken = AppDispatcher.register((action) => {
if (action.type === 'ADD') {
value = value + action.value;
store.emitChange();
}
// your codes
});
export default store;
view組件中的state應(yīng)該與Flux store保持一致,如下:
function MyView extend Component {
constructor(props){
this.state = { count: store.getValue()}
}
// 聲明周期函數(shù)(組件加載和卸載),需要調(diào)用store的事件注冊(cè)函數(shù),
// 將處理組件state變化的函數(shù)設(shè)置為注冊(cè)函數(shù)的回調(diào)方法
componentDidMount() {
store.addChangeListener(this.onChange);
}
componentWillUnmount() {
store.removeChangeListener(this.onChange);
}
onChange() {
const newCount = store.getValue();
this.setState({count: newCount});
}
// 組件的事件函數(shù),需要調(diào)用Action觸發(fā)狀態(tài)更新
onClickIncrementButton() {
Actions.increment(text);
}
}
Flux的缺點(diǎn)為:
- 一個(gè)應(yīng)用可以擁有多個(gè)store,多個(gè)store直接可能有依賴關(guān)系(相互引用);
- Store封裝了數(shù)據(jù)和處理數(shù)據(jù)的邏輯。
針對(duì)Flux的不足,Redux框架出現(xiàn)。
Redux
相比Flux,Redux有如下兩個(gè)特點(diǎn):
- 在整個(gè)應(yīng)用只提供一個(gè)Store,它是一個(gè)扁平的樹形結(jié)構(gòu),一個(gè)節(jié)點(diǎn)狀態(tài)應(yīng)該只屬于一個(gè)組件。
- 不允許修改數(shù)據(jù)。即不能修改老狀態(tài),只能返回一個(gè)新狀態(tài)。
Redux數(shù)據(jù)流如下(來自網(wǎng)絡(luò)):
不同于 Flux ,Redux 并沒有 dispatcher 的概念(Store已經(jīng)集成了dispatch方法,所以不需要Dispatcher)。它依賴純函數(shù)來替代事件處理器,這個(gè)純函數(shù)叫做Reducer。Reducer在Redux里是個(gè)很重要的概念,其封裝了處理數(shù)據(jù)的邏輯。
在計(jì)算機(jī)編程中,假如滿足下面這兩個(gè)句子的約束,一個(gè)函數(shù)可能被描述為一個(gè)純函數(shù):
1. 給出同樣的參數(shù)值,該函數(shù)總是求出同樣的結(jié)果。
該函數(shù)結(jié)果值不依賴任何隱藏信息或程序執(zhí)行處理可能改變的狀態(tài)或在程序的兩個(gè)不同的執(zhí)行。
2. 結(jié)果的求值不會(huì)促使任何可語(yǔ)義上可觀察的副作用或輸出。
簡(jiǎn)單說,一個(gè)純函數(shù),只要輸入相同,無(wú)論調(diào)用多少次,輸出都是一樣的。這就要求,絕不能修改輸入?yún)?shù),因?yàn)檩斎雲(yún)?shù)有可能在其他地方用到。下面是一個(gè)簡(jiǎn)單的Reducer對(duì)象:
export default (state, action) => {
switch (action.type) {
case 'INCREMENT':
return {...state, value: action.value + 1};
default:
return state;
}
}
把數(shù)據(jù)邏輯分離開后,Store就變得簡(jiǎn)單了。它的構(gòu)造函數(shù)需要一個(gè)reducer對(duì)象(每個(gè)組件對(duì)應(yīng)一個(gè)reducer,通過combineReducers
函數(shù)合并N個(gè)子reducer為一個(gè)主reducer),初始化數(shù)據(jù),和中間件(可選)。由于Store已經(jīng)集成了dispatch方法,所以不需要Dispatcher。一個(gè)簡(jiǎn)單的Store如下:
import {createStore, combineReducers} from 'redux';
const reducer = combineReducers({
reducer: incrementReducer
});
export default createStore(reducer, {});
View層通過store.dispatch
觸發(fā)動(dòng)作:
onIncrement() {
store.dispatch(Actions.increment(value));
}
組件 Context
看到這里,可以發(fā)現(xiàn)Flux和Redux都需要顯性的在View里面引入store, import store from './Store'
。如果可以在一個(gè)應(yīng)用中,只引入一次store,然后所有組件都可以訪問到,那該多好?!非常幸運(yùn),React提供了這樣的功能,即Context。
Context就是“上下文環(huán)境”,讓一個(gè)數(shù)狀組件上所有組件都能訪問一個(gè)共有的對(duì)象。
讓頂層容器組件支持Context,那么子組件都可以訪問到store,無(wú)需各自import??梢匀缦露x一個(gè)頂層組件:
import {PropTypes, Component} from 'react';
class Provider extends Component {
//必須實(shí)現(xiàn)getChildContext方法
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
Provider.propTypes = {
store: PropTypes.object.isRequired
}
// 必須定義靜態(tài)屬性childContextTypes ,和getChildContext()對(duì)應(yīng)
Provider.childContextTypes = {
store: PropTypes.object
};
export default Provider;
在入口文件內(nèi)使用頂層組件:
import React from 'react';
import ReactDOM from 'react-dom';
import store from './Store.js';
import Provider from './Provider.js';
ReactDOM.render(
<Provider store={store}>
...
</Provider>,
document.getElementById('root')
);
所有子組件對(duì)象都可直接訪問到store對(duì)象:
const value = this.context.store.getState();
react-redux
要聲明一點(diǎn),Redux并不是專為React開發(fā)的,它可以應(yīng)用在任何框架上。針對(duì)React工程,可以使用react-redux庫(kù)幫助我們更快,更便捷得搭建Redux工程,讓代碼更加精簡(jiǎn)。react-redux庫(kù)提供了如下功能:
- 把組件拆分為容器組件和傻瓜組件,使用者只需要寫傻瓜組件;
- 使用React的Context提供了一個(gè)所有組件都可以直接訪問的Context,即react-redux Provider;
于是,我們不需要自己寫頂層組件了,只要導(dǎo)入react-redux的Provider,如下:
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import store from './Store.js';
ReactDOM.render(
<Provider store={store}>
...
</Provider>,
document.getElementById('root')
);
Action和Store寫法不變,與Redux相同。
組件變得更加簡(jiǎn)潔,如下:
function Counter({caption, onIncrement, onDecrement, value}) {
return (
<div>
<button style={buttonStyle} onClick={onIncrement}>+</button>
<span>{caption} count: {value}</span>
</div>
);
}
//store中狀態(tài)state到傻瓜組件屬性props的映射
function mapStateToProps(state, ownProps) {
return {
value: state[ownProps.caption]
}
}
//傻瓜組件中用戶的每個(gè)動(dòng)作,都轉(zhuǎn)換為派送給store的動(dòng)作
function mapDispatchToProps(dispatch, ownProps) {
return {
onIncrement: () => {
dispatch(Actions.increment(ownProps.caption));
}
}
}
// connent函數(shù):連接容器組件和傻瓜組件
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
可以看到,用了react-redux之后,代碼精簡(jiǎn)不少,而且邏輯更加清晰。
小結(jié)
從Flux到Redux,再到react-redux,從這個(gè)簡(jiǎn)短歷程中,我們可以看到框架設(shè)計(jì)上的演進(jìn),而redux + react-redux也是React開發(fā)萬(wàn)家桶的標(biāo)配。到了這里,可以忘記Flux啦~