Redux介绍之React-Redux

16 May

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

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小书的图,一图胜千言:

redux

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

评论(4)

    • 我觉得不必太纠结了细节了。Provider也是个component,封装了React的context代码,消除了重复代码,我将它也划入HOC之列。其实我本想更激进地将HOC解释成父类,后来想想算了,和父类还不完全一样。找到自己合适的理解就OK

Leave a Reply

Your email address will not be published. Required fields are marked *