Redux介绍之React-Redux

作者: 张歆琳 | 来源:发表于2017-05-16 00:28 被阅读896次

    我们已经详细介绍了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也没问题,但最佳实践还是遵守比较好,否则业务代码会比较混乱。

    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组件,非常方便。

    相关文章

      网友评论

        本文标题:Redux介绍之React-Redux

        本文链接:https://www.haomeiwen.com/subject/roukxxtx.html