我們已經詳細介紹了Action,Reducer,Store和它們之間的流轉關系先馆。Redux的基礎知識差不多也介紹完了。前幾篇的源代碼中雖然用到了React躺彬,其實你會發(fā)現(xiàn)源碼中React和Redux毫無關系煤墙,用React僅僅是因為寫DOM元素方便。Redux不是React專用宪拥,它也可以支持其他框架仿野。但本人水平有限,并未在其他框架下(jQuery不算)使用過Redux她君。本篇介紹一下如何在React里使用Redux脚作。源碼已上傳Github,請參照src/reactRedux文件夾缔刹。
- Provider
- connect之mapStateToProps
- connect之mapDispatchToProps
- connect之mergeProps
- 實現(xiàn)原理
先要安裝一下react-redux包:
yarn add –D react-redux
根據(jù)官網(wǎng)推薦將React組件分為容器組件container和展示組件component鳖枕。為了使代碼結構更加合理魄梯,我們如下圖,在項目根目錄里新建container和component目錄宾符。container目錄里的組件需要關心Redux酿秸。而component目錄里的組件僅做展示用,不需要關心Redux魏烫。這是一種最佳實踐辣苏,并沒有語法上的強制規(guī)定,因此component目錄的組件綁定Redux也沒問題哄褒,但最佳實踐還是遵守比較好稀蟋,否則業(yè)務代碼會比較混亂。
containers目錄下的sample組件會關聯(lián)Redux怀泊,更新完的數(shù)據(jù)作為alert和number組件的props傳遞給它們茫藏。
<Provider store>
組件都被抽出后,原本entries目錄下的文件中還剩下什么呢霹琼?entries/reactRedux.js:
import { Provider } from 'react-redux'; // 引入 react-redux
……
render(
<Provider store={store}>
<Sample />
</Provider>,
document.getElementById('app'),
);
react-redux包一共就兩個API:<Provider store>和connect方法务傲。在React框架下使用Redux的第一步就是將入口組件包進里,store指定通過createStore生成出來的Store枣申。只有這樣售葡,被包進的組件及子組件才能訪問到Store,才能使用connect方法忠藤。
入口解決了挟伙,我們看一下sample組件是如何用connect方法關聯(lián)Redux的。先看一下connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])方法熄驼,簽名有點長像寒,參照containers/sample/sample.js:
const mapStateToProps = (state) => {
return {
number: state.changeNumber.number,
showAlert: state.toggleAlert.showAlert,
};
};
const mapDispatchToProps = {
incrementNum: action.number.incrementNum,
decrementNum: action.number.decrementNum,
clearNum: action.number.clearNum,
toggleAlert: action.alert.toggleAlert,
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(Sample);
connect之mapStateToProps
connect的第一個參數(shù)mapStateToProps是一個function:[mapStateToProps(state, [ownProps]): stateProps]烘豹,作用是負責輸入瓜贾,將Store里的state變成組件的props。函數(shù)返回值是一個key-value的plain object携悯。例子代碼里是:
const mapStateToProps = (state) => {
return {
number: state.changeNumber.number,
showAlert: state.toggleAlert.showAlert,
};
};
函數(shù)返回值是一個將state和組件props建立了映射關系的plain object祭芦。你可以這樣理解:connect的第一個參數(shù)mapStateToProps就是輸入。將state綁定到組件的props上憔鬼。這樣會自動Store.subscribe組件龟劲。當建立了映射關系的state更新時胃夏,會調用mapStateToProps同步更新組件的props值,觸發(fā)組件的render方法昌跌。
如果mapStateToProps為空(即設成()=>({}))仰禀,那Store里的任何更新就不會觸發(fā)組件的render方法。
mapStateToProps方法還支持第二個可選參數(shù)ownProps蚕愤,看名字就知道是組件自己原始的props(即不包含connect后新增的props)答恶。例子代碼因為比較簡單,沒有用到ownProps萍诱⌒ぃ可以YY一個例子:
const mapStateToProps = (state, ownProps) => {
// state 是 {userList: [{id: 0, name: 'Jack'}, ...]}
return {
isMe: state.userList.includes({id: ownProps.userId})
};
}
當state或ownProps更新時,mapStateToProps都會被調用裕坊,更新組件的props值包竹。
connect之mapDispatchToProps
connect的第二個參數(shù)mapDispatchToProps可以是一個object也可以是一個function,作用是負責輸出籍凝,將Action creator綁定到組件的props上周瞎,這樣組件就能派發(fā)Action,更新state了静浴。當它為object時堰氓,應該是一個key-value的plain object,key是組件props苹享,value是一個Action creator双絮。例子代碼里就采用了這個方式:
const mapDispatchToProps = {
incrementNum: action.number.incrementNum,
decrementNum: action.number.decrementNum,
clearNum: action.number.clearNum,
toggleAlert: action.alert.toggleAlert,
};
將定義好的Action creator映射成組件的porps,這樣就能在組件中通過this.props. incrementNum()
方式來dispatch Action出去得问,通知Reducer修改state囤攀。如果你對Action比較熟悉的話,可能會疑惑宫纬,this.props.incrementNum()
只是生成了一個Action焚挠,應該是寫成:dispatch(this.props. incrementNum())
才對吧?繼續(xù)看下面介紹的function形式的mapDispatchToProps就能明白漓骚,其實dispatch已經被connect封裝進去了蝌衔,因此你不必手動寫dispatch了。
mapDispatchToProps還可以是一個function:[mapDispatchToProps(dispatch, [ownProps]): dispatchProps]蝌蹂。改寫例子代碼:
import { bindActionCreators } from 'redux';
const mapDispatchToProps2 = (dispatch, ownProps) => {
return {
incrementNum: bindActionCreators(action.number.incrementNum, dispatch),
decrementNum: bindActionCreators(action.number.decrementNum, dispatch),
clearNum: bindActionCreators(action.number.clearNum, dispatch),
toggleAlert: bindActionCreators(action.alert.toggleAlert, dispatch),
};
};
這段代碼和例子代碼中的object形式的mapDispatchToProps是等價的噩斟。世上并沒有自動的事,所謂的自動只不過是connet中封裝了Store.dispatch而已孤个。
第一個參數(shù)是dispatch剃允,第二個可選參數(shù)ownProps和mapStateToProps里作用是一樣的,不贅述。
connect之mergeProps
我們現(xiàn)在已經知道斥废,經過conncet的組件的props有3個來源:一是由mapStateToProps將state映射成的props椒楣,二是由mapDispatchToProps將Action creator映射成的props,三是組件自身的props牡肉。
connect的第三個參數(shù)mergeProps也是一個function:[mergeProps(stateProps, dispatchProps, ownProps): props]捧灰,參數(shù)分別對應了上述props的3個來源,作用是整合這些props统锤。例如過濾掉不需要的props:
const mergeProps = (stateProps, dispatchProps, ownProps) => {
return {
...ownProps,
...stateProps,
incrementNum: dispatchProps.incrementNum, // 只輸出incrementNum
};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
)(Sample);
這樣你組件里就無法從props里取到decrementNum和clearNum了凤壁。再例如重新組織props:
const mergeProps = (stateProps, dispatchProps, ownProps) => {
return {
...ownProps,
state: stateProps,
actions: {
...dispatchProps,
...ownProps.actions,
},
};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
)(Sample);
這樣你代碼里無法this.props.incrementNum()
這樣調用,要改成this.props.actions.incrementNum()
這樣調用跪另。
至此react-redux的內容就介紹完了拧抖,一共就兩個API:
<Provider store>用于在入口處包裹需要用到Redux的組件。
conncet方法用于將組件綁定Redux免绿。第一個參數(shù)負責輸入唧席,將state映射成組件props。第二個參數(shù)負責輸出嘲驾,允許組件去改變state的值淌哟。第三個參數(shù)甚至都沒什么出鏡率,例子代碼就沒有用到這個參數(shù)辽故,可以讓程序員自己調整組件的props徒仓。
實現(xiàn)原理
接下來介紹一下react-redux的實現(xiàn)原理,需要一定React基礎誊垢,如果你能看懂相必是極好的台颠。但如果你只想使用react-redux的話鞋喇,上述內容就足夠了,下面的部分看不懂也沒關系。
我們知道React里有個全局變量context劳殖,它其實和React一切皆組件的設計思路不符洲鸠。但實際開發(fā)中蝇更,組件間嵌套層次比較深時幽崩,傳遞數(shù)據(jù)真的是比較麻煩√兀基于此奈惑,React提供了個類似后門的全局變量context∷冢可用將組件間共享的數(shù)據(jù)放到contex里肴甸,這樣做的優(yōu)點是:所有組件都可以隨時訪問到context里共享的值,免去了數(shù)據(jù)層層傳遞的麻煩帮孔,非常方便雷滋。缺點是:和所有其他語言一樣,全局變量意味著所有人都可以隨意修改它文兢,導致不可控晤斩。
Redux恰好需要一個全局的Store,那在React框架里姆坚,將Store存入context中再合適不過了澳泵,所有組件都能隨時訪問到context里的Store。而且Redux規(guī)定了只能通過dispatch Action來修改Store里的數(shù)據(jù)兼呵,因此規(guī)避了所有人都可以隨意修改context值的缺點兔辅。完美。
理解了這層击喂,再回頭看<Provider store>维苔,它的作用是將createStore生成的store保存進context。這樣被它包裹著的子組件都可以訪問到context里的Store懂昂。
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class Provider extends Component {
static contextTypes = {
store: PropTypes.object,
children: PropTypes.any,
};
static childContextTypes = {
store: PropTypes.object,
};
getChildContext = () => {
return { store: this.props.store, };
};
render () {
return (<div>{this.props.children}</div>);
}
}
經過conncet后的組件是一個HOC高階組件(High-order Component)介时,參照React.js小書的圖,一圖勝千言:
第一步:內部封裝掉了每個組件都要寫的訪問context的代碼:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const connect = (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object,
};
render() {
return <WrappedComponent />
}
}
return Connect;
};
export default connect;
第二步:封裝掉subscribe段审,當store變化,刷新組件的props闹蒜,觸發(fā)組件的render方法
const connect = (WrappedComponent) => {
class Connect extends Component {
...
constructor() {
super();
this.state = { allProps: {} }
}
componentWillMount() {
const { store } = this.context;
this._updateProps();
store.subscribe(this._updateProps);
}
_updateProps = () => {
this.setState({
allProps: {
// TBD
...this.props,
}
});
};
render () {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect;
};
第三步:參數(shù)mapStateToProps封裝掉組件從context中取Store的代碼
export const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
...
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps(store.getState());
this.setState({
allProps: {
...stateProps,
...this.props
}
})
}
...
}
return Connect
}
第四步:參數(shù)mapDispatchToProps封裝掉組件往context里更新Store的代碼
export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
...
_updateProps () {
const { store } = this.context
let stateProps = mapStateToProps(store.getState());
let dispatchProps = mapDispatchToProps(store.dispatch);
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}
...
}
return Connect
}
完整版:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object,
};
constructor() {
super();
this.state = { allProps: {} }
}
componentWillMount() {
const { store } = this.context;
this._updateProps();
store.subscribe(this._updateProps);
}
_updateProps = () => {
const { store } = this.context;
let stateProps = mapStateToProps(store.getState());
let dispatchProps = mapDispatchToProps(store.dispatch);
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props,
}
});
};
render () {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect;
};
export default connect;
明白了原理后寺枉,再次總結一下react-redux:
<Provider store>用于在入口處包裹需要用到Redux的組件。本質上是將store放入context里绷落。
conncet方法用于將組件綁定Redux姥闪。本質上是HOC,封裝掉了每個組件都要寫的板式代碼砌烁。
react-redux的高封裝性讓開發(fā)者感知不到context的存在筐喳,甚至感知不到Store的getState催式,subscribe,dispatch的存在避归。只要connect一下荣月,數(shù)據(jù)一變就自動刷新React組件,非常方便梳毙。