Redux介紹之React-Redux

我們已經詳細介紹了Action,Reducer,Store和它們之間的流轉關系先馆。Redux的基礎知識差不多也介紹完了。前幾篇的源代碼中雖然用到了React躺彬,其實你會發(fā)現(xiàn)源碼中React和Redux毫無關系煤墙,用React僅僅是因為寫DOM元素方便。Redux不是React專用宪拥,它也可以支持其他框架仿野。但本人水平有限,并未在其他框架下(jQuery不算)使用過Redux她君。本篇介紹一下如何在React里使用Redux脚作。源碼已上傳Github,請參照src/reactRedux文件夾缔刹。

  • Provider
  • connect之mapStateToProps
  • connect之mapDispatchToProps
  • connect之mergeProps
  • 實現(xiàn)原理

先要安裝一下react-redux包:

yarn add –D react-redux

根據(jù)官網(wǎng)推薦將React組件分為容器組件container和展示組件component鳖枕。為了使代碼結構更加合理魄梯,我們如下圖,在項目根目錄里新建container和component目錄宾符。container目錄里的組件需要關心Redux酿秸。而component目錄里的組件僅做展示用,不需要關心Redux魏烫。這是一種最佳實踐辣苏,并沒有語法上的強制規(guī)定,因此component目錄的組件綁定Redux也沒問題哄褒,但最佳實踐還是遵守比較好稀蟋,否則業(yè)務代碼會比較混亂。

components目錄下放兩個供展示用的alert和number組件呐赡,這兩個組件完全不會感知到Redux的存在退客,它們依賴傳入的props變化,來觸發(fā)自身的render方法链嘀。本系列不是React教程萌狂,React組件的代碼請自行參照源碼。

containers目錄下的sample組件會關聯(lián)Redux怀泊,更新完的數(shù)據(jù)作為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方法關聯(lián)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的第一個參數(shù)mapStateToProps是一個function:[mapStateToProps(state, [ownProps]): stateProps]烘豹,作用是負責輸入瓜贾,將Store里的state變成組件的props。函數(shù)返回值是一個key-value的plain object携悯。例子代碼里是:

const mapStateToProps = (state) => {
    return {
        number: state.changeNumber.number,
        showAlert: state.toggleAlert.showAlert,
    };
};

函數(shù)返回值是一個將state和組件props建立了映射關系的plain object祭芦。你可以這樣理解:connect的第一個參數(shù)mapStateToProps就是輸入。將state綁定到組件的props上憔鬼。這樣會自動Store.subscribe組件龟劲。當建立了映射關系的state更新時胃夏,會調用mapStateToProps同步更新組件的props值,觸發(fā)組件的render方法昌跌。

如果mapStateToProps為空(即設成()=>({}))仰禀,那Store里的任何更新就不會觸發(fā)組件的render方法。

mapStateToProps方法還支持第二個可選參數(shù)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的第二個參數(shù)mapDispatchToProps可以是一個object也可以是一個function,作用是負責輸出籍凝,將Action creator綁定到組件的props上周瞎,這樣組件就能派發(fā)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())才對吧?繼續(xù)看下面介紹的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而已孤个。

第一個參數(shù)是dispatch剃允,第二個可選參數(shù)ownProps和mapStateToProps里作用是一樣的,不贅述。

connect之mergeProps

我們現(xiàn)在已經知道斥废,經過conncet的組件的props有3個來源:一是由mapStateToProps將state映射成的props椒楣,二是由mapDispatchToProps將Action creator映射成的props,三是組件自身的props牡肉。

connect的第三個參數(shù)mergeProps也是一個function:[mergeProps(stateProps, dispatchProps, ownProps): props]捧灰,參數(shù)分別對應了上述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免绿。第一個參數(shù)負責輸入唧席,將state映射成組件props。第二個參數(shù)負責輸出嘲驾,允許組件去改變state的值淌哟。第三個參數(shù)甚至都沒什么出鏡率,例子代碼就沒有用到這個參數(shù)辽故,可以讓程序員自己調整組件的props徒仓。

實現(xiàn)原理

接下來介紹一下react-redux的實現(xiàn)原理,需要一定React基礎誊垢,如果你能看懂相必是極好的台颠。但如果你只想使用react-redux的話鞋喇,上述內容就足夠了,下面的部分看不懂也沒關系。

我們知道React里有個全局變量context劳殖,它其實和React一切皆組件的設計思路不符洲鸠。但實際開發(fā)中蝇更,組件間嵌套層次比較深時幽崩,傳遞數(shù)據(jù)真的是比較麻煩√兀基于此奈惑,React提供了個類似后門的全局變量context∷冢可用將組件間共享的數(shù)據(jù)放到contex里肴甸,這樣做的優(yōu)點是:所有組件都可以隨時訪問到context里共享的值,免去了數(shù)據(jù)層層傳遞的麻煩帮孔,非常方便雷滋。缺點是:和所有其他語言一樣,全局變量意味著所有人都可以隨意修改它文兢,導致不可控晤斩。

Redux恰好需要一個全局的Store,那在React框架里姆坚,將Store存入context中再合適不過了澳泵,所有組件都能隨時訪問到context里的Store。而且Redux規(guī)定了只能通過dispatch Action來修改Store里的數(shù)據(jù)兼呵,因此規(guī)避了所有人都可以隨意修改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 傳遞數(shù)據(jù)先鱼。即讓connect和context打交道徒蟆,然后通過 props 把參數(shù)傳給組件自身。我們來實現(xiàn)一下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闹蒜,觸發(fā)組件的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;
};

第三步:參數(shù)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
}

第四步:參數(shù)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的高封裝性讓開發(fā)者感知不到context的存在筐喳,甚至感知不到Store的getState催式,subscribe,dispatch的存在避归。只要connect一下荣月,數(shù)據(jù)一變就自動刷新React組件,非常方便梳毙。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末哺窄,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子账锹,更是在濱河造成了極大的恐慌萌业,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奸柬,死亡現(xiàn)場離奇詭異生年,居然都是意外死亡,警方通過查閱死者的電腦和手機廓奕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門晶框,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人懂从,你說我怎么就攤上這事授段。” “怎么了番甩?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵侵贵,是天一觀的道長。 經常有香客問我缘薛,道長窍育,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任宴胧,我火速辦了婚禮漱抓,結果婚禮上,老公的妹妹穿的比我還像新娘恕齐。我一直安慰自己乞娄,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布显歧。 她就那樣靜靜地躺著仪或,像睡著了一般。 火紅的嫁衣襯著肌膚如雪士骤。 梳的紋絲不亂的頭發(fā)上范删,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音拷肌,去河邊找鬼到旦。 笑死旨巷,一個胖子當著我的面吹牛,可吹牛的內容都是我干的添忘。 我是一名探鬼主播采呐,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼昔汉!你這毒婦竟也來了?” 一聲冷哼從身側響起拴清,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤靶病,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后口予,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娄周,經...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年沪停,在試婚紗的時候發(fā)現(xiàn)自己被綠了煤辨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡木张,死狀恐怖众辨,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情舷礼,我是刑警寧澤鹃彻,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站妻献,受9級特大地震影響蛛株,放射性物質發(fā)生泄漏。R本人自食惡果不足惜育拨,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一谨履、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧熬丧,春花似錦笋粟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嫌变,卻和暖如春吨艇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腾啥。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工东涡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留冯吓,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓疮跑,卻偏偏與公主長得像组贺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子祖娘,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容

  • 做React需要會什么渐苏? react的功能其實很單一掀潮,主要負責渲染的功能,現(xiàn)有的框架琼富,比如angular是一個大而...
    蒼都閱讀 14,759評論 1 139
  • 前言 本文 有配套視頻仪吧,可以酌情觀看。 文中內容因各人理解不同鞠眉,可能會有所偏差薯鼠,歡迎朋友們聯(lián)系我討論。 文中所有內...
    珍此良辰閱讀 11,904評論 23 111
  • 今天來看一下react組件之間是怎么進行通訊的械蹋。react推崇的是單向數(shù)據(jù)流出皇,自上而下進行數(shù)據(jù)的傳遞,但是由下而上...
    親親qin閱讀 6,005評論 2 12
  • React 在 React 中哗戈,UI 以組件的形式來搭建恶迈,組件之間可以嵌套組合。另谱醇,React 中組件間通信的數(shù)據(jù)...
    RN學習閱讀 1,021評論 0 6
  • 學習必備要點: 首先弄明白暇仲,Redux在使用React開發(fā)應用時,起到什么作用——狀態(tài)集中管理 弄清楚Redux是...
    賀賀v5閱讀 8,896評論 10 58