Redux介绍之React-Redux

我们已经详细介绍了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组件,非常方便。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容