我们已经详细介绍了Action,Reducer,Store和它们之间的流转关系。Redux的基础知识差不多也介绍完了。前几篇的源代码中虽然用到了React,其实你会发现源码中React和Redux毫无关系,用React仅仅是因为写DOM元素方便。Redux不是React专用,它也可以支持其他框架。但本人水平有限,并未在其他框架下(jQuery不算)使用过Redux。本篇介绍一下如何在React里使用Redux。源码已上传Github,请参照src/reactRedux文件夹。
- Provider
- connect之mapStateToProps
- connect之mapDispatchToProps
- connect之mergeProps
- 实现原理
先要安装一下react-redux包:
yarn add –D react-redux
根据官网推荐将React组件分为容器组件container和展示组件component。为了使代码结构更加合理,我们如下图,在项目根目录里新建container和component目录。container目录里的组件需要关心Redux。而component目录里的组件仅做展示用,不需要关心Redux。这是一种最佳实践,并没有语法上的强制规定,因此component目录的组件绑定Redux也没问题,但最佳实践还是遵守比较好,否则业务代码会比较混乱。
containers目录下的sample组件会关联Redux,更新完的数据作为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方法关联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的第一个参数mapStateToProps是一个function:[mapStateToProps(state, [ownProps]): stateProps],作用是负责输入,将Store里的state变成组件的props。函数返回值是一个key-value的plain object。例子代码里是:
const mapStateToProps = (state) => {
return {
number: state.changeNumber.number,
showAlert: state.toggleAlert.showAlert,
};
};
函数返回值是一个将state和组件props建立了映射关系的plain object。你可以这样理解:connect的第一个参数mapStateToProps就是输入。将state绑定到组件的props上。这样会自动Store.subscribe组件。当建立了映射关系的state更新时,会调用mapStateToProps同步更新组件的props值,触发组件的render方法。
如果mapStateToProps为空(即设成()=>({})),那Store里的任何更新就不会触发组件的render方法。
mapStateToProps方法还支持第二个可选参数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的第二个参数mapDispatchToProps可以是一个object也可以是一个function,作用是负责输出,将Action creator绑定到组件的props上,这样组件就能派发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())
才对吧?继续看下面介绍的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而已。
第一个参数是dispatch,第二个可选参数ownProps和mapStateToProps里作用是一样的,不赘述。
connect之mergeProps
我们现在已经知道,经过conncet的组件的props有3个来源:一是由mapStateToProps将state映射成的props,二是由mapDispatchToProps将Action creator映射成的props,三是组件自身的props。
connect的第三个参数mergeProps也是一个function:[mergeProps(stateProps, dispatchProps, ownProps): props],参数分别对应了上述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。第一个参数负责输入,将state映射成组件props。第二个参数负责输出,允许组件去改变state的值。第三个参数甚至都没什么出镜率,例子代码就没有用到这个参数,可以让程序员自己调整组件的props。
实现原理
接下来介绍一下react-redux的实现原理,需要一定React基础,如果你能看懂相必是极好的。但如果你只想使用react-redux的话,上述内容就足够了,下面的部分看不懂也没关系。
我们知道React里有个全局变量context,它其实和React一切皆组件的设计思路不符。但实际开发中,组件间嵌套层次比较深时,传递数据真的是比较麻烦。基于此,React提供了个类似后门的全局变量context。可用将组件间共享的数据放到contex里,这样做的优点是:所有组件都可以随时访问到context里共享的值,免去了数据层层传递的麻烦,非常方便。缺点是:和所有其他语言一样,全局变量意味着所有人都可以随意修改它,导致不可控。
Redux恰好需要一个全局的Store,那在React框架里,将Store存入context中再合适不过了,所有组件都能随时访问到context里的Store。而且Redux规定了只能通过dispatch Action来修改Store里的数据,因此规避了所有人都可以随意修改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,触发组件的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;
};
第三步:参数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
}
第四步:参数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的高封装性让开发者感知不到context的存在,甚至感知不到Store的getState,subscribe,dispatch的存在。只要connect一下,数据一变就自动刷新React组件,非常方便。