我们已经详细介绍了Action,Reducer,Store和它们之间的流转关系。Redux的基础知识差不多也介绍完了。前几篇的源代码中虽然用到了React,其实你会发现源码中React和Redux毫无关系,用React仅仅是因为写DOM元素方便。Redux不是React专用,它也可以支持其他框架。但本人水平有限,并未在其他框架下(jQuery不算)使用过Redux。本篇介绍一下如何在React里使用Redux。源码已上传Github,请参照src/reactRedux文件夹。
- <Provider store>
- connect之mapStateToProps
- connect之mapDispatchToProps
- connect之mergeProps
- 实现原理
先要安装一下react-redux包:
yarn add –D react-redux
根据官网推荐将React组件分为容器组件container和展示组件component。为了使代码结构更加合理,我们如下图,在项目根目录里新建container和component目录。container目录里的组件需要关心Redux。而component目录里的组件仅做展示用,不需要关心Redux。这是一种最佳实践,并没有语法上的强制规定,因此component目录的组件绑定Redux也没问题,但最佳实践还是遵守比较好,否则业务代码会比较混乱。
components目录下放两个供展示用的alert和number组件,这两个组件完全不会感知到Redux的存在,它们依赖传入的props变化,来触发自身的render方法。本系列不是React教程,React组件的代码请自行参照源码。
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小书的图,一图胜千言:
HOC高阶组件听上去名字比较吓人,不像人话,我第一次听到的反映也是“什么鬼?”。但其实原理不复杂,说穿了就是为了消除重复代码用的。有些代码每个组件都要重复写(例如getChildContext),干脆将它们抽取出来写到一个组件内,这个组件就是高阶组件。高阶组件内部的包装组件和被包装组件之间通过 props 传递数据。即让connect和context打交道,然后通过props把参数传给组件自身。我们来实现一下connect。
第一步:内部封装掉了每个组件都要写的访问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组件,非常方便。