一塊兒學習造輪子(三):從零開始寫一個React-Redux

本文是一塊兒學習造輪子系列的第三篇,本篇咱們將從零開始寫一個React-Redux,本系列文章將會選取一些前端比較經典的輪子進行源碼分析,而且從零開始逐步實現,本系列將會學習Promises/A+,Redux,react-redux,vue,dom-diff,webpack,babel,kao,express,async/await,jquery,Lodash,requirejs,lib-flexible等前端經典輪子的實現方式,每一章源碼都託管在github上,歡迎關注~

相關係列文章:

一塊兒學習造輪子(一):從零開始寫一個符合Promises/A+規範的promise

一塊兒學習造輪子(二):從零開始寫一個Redux

一塊兒學習造輪子(三):從零開始寫一個React-Redux

本系列github倉庫:

一塊兒學習造輪子系列github(歡迎star~)html

前言

上一章咱們寫了一個redux,當redux與react結合時通常爲了方便會使用react-redux,
這個庫是能夠選用的。實際項目中,應該權衡一下,是直接使用 Redux,仍是使用 React-Redux。後者雖然提供了便利,可是須要掌握額外的 API,而且要遵照它的組件拆分規範。
本文對於react-redux的用法不會過多介紹,重點仍然放在源碼實現上。若是還不太瞭解如何使用,能夠看相關文章學習。
前端

推薦文章:
vue

Redux 入門教程:React-Redux 的用法react

本文全部代碼在github建有代碼倉庫,能夠點此查看本文代碼,也歡迎你們star~jquery

開始

context

講React-Redux前,咱們先來說一下React.js裏的context。React.js裏的context一直被視爲一個不穩定的、危險的、可能會被去掉的特性而不被官網文檔所記載,可是使用它卻很是方便,好比說咱們有一棵很龐大的組件樹,在咱們沒有使用redux時咱們想要改變一個狀態並讓全部組件生效,咱們須要一層一層的往下傳props。可是有了context就很簡單了。某個組件只要往本身的context裏面放了某些狀態,這個組件之下的全部子組件均可以直接訪問這個狀態而不須要經過中間組件的傳遞。webpack

例若有這麼一棵組件樹:git

props傳遞

userinfo用戶信息這個數據是不少組件都須要用的,因此咱們按照正常的思路在根節點的 Index 上獲取,而後把這個狀態經過 props一層層傳遞下去,最終全部組件都拿到了userinfo,進行使用。
可是這樣有個問題:
github

若是組件層級很深的話,用props向下傳值就是災難。
web

咱們想,若是這顆組件樹可以全局共享這個一個狀態倉庫就行了,咱們要的時候就去狀態倉庫裏取對應的狀態,不用手動地傳,這該多好啊。
全局狀態
React.js 的 context 就是這麼一個東西,某個組件只要往本身的 context 裏面放了某些狀態,這個組件之下的全部子組件都直接訪問這個狀態而不須要經過中間組件的傳遞,來看下具體怎麼用:express

//在根組件上將userInfo放入context
class Index extends Component {
    static childContextTypes = {
        userInfo: PropTypes.object
    }

    constructor() {
        super()
        this.state = { 
            userInfo: {
                name:"小明",
                id:17
                } 
        }
    }

    getChildContext() {
        return { userInfo: this.state.userInfo }
    }

    render() {
        return ( <div >
                    <Header/>
                </div>
        )
    }
}

class Header extends Component {
    render() {
        return ( <div>
                <Title/>
            </div>
        )
    }
}
class Title extends Component {
    static contextTypes = {
        title: PropTypes.object
    }
    render() {
        // 不管組件層級有多深,子組件均可以直接從context屬性獲取狀態
        return ( <h1> 歡迎{ this.context.userInfo.name } </h1>)
    }
}

上面,咱們將userInfo定義在了根組件Index上,而且將它掛載到Index的context上,以後不管下面有多少層子組件,均可以直接從context上獲取這個title狀態了。

那麼既然context用着這麼方便還用redux管理全局狀態幹什麼?

由於context裏面的數據能被隨意接觸就能被隨意修改,致使程序運行的不可預料。這也是context一直不建議使用的緣由,而redux雖然使用起來很麻煩,可是卻能作到修改數據的行爲變得可預測可追蹤,由於在redux裏你必須經過dispatch執行某些容許的修改操做,並且必須事先在action裏面明確聲明要作的操做。

那麼咱們能不能結合一下兩者的優勢,使咱們能夠既安全又容易的來管理全局狀態呢?

React-Redux

react-redux
React-Redux是Redux的做者封裝了一個 React 專用的庫 ,爲了能讓React使用者更方便的使用Redux,廢話很少說,咱們平時在使用React-Redux時通常這樣寫:

// root.js
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import userReducer from 'reducers/userReducer'
import Header from 'containers/header'
const store = createStore(userReducer)

export default class Root extends Component {
    render() {
        return (<div>
                    <Header></Header>
                </div>
        );
    };
}
ReactDOM.render( <Provider store = { store } >
                        <Root/>
                </Provider>, 
document.getElementById('root'));


//containers/header.js
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as userinfoActions from 'actions/userinfo.js';
import fetch from 'isomorphic-fetch'

class Header extends Component {
    constructor() {
        super();
        this.state = {
            username:""
        }
    }
    componentDidMount(){
        this.getUserInfo()
    }
    getUserInfo(){
        fetch("/api/pay/getUserInfo")
            .then(response => {
                return response.json()
            })
            .then(json =>{
                this.props.userinfoActions.login(data);
                this.setState({username: data.username});
            })
            .catch(e => {
                console.log(e)
            })
    }
    render(){
         return (
            <div>
                歡迎用戶{this.state.username}
            </div>
        );
    }
}

function mapStateToProps(state) {
    return { userinfo: state.userinfo }
}

function mapDispatchToProps(dispatch) {
    return {
        userinfoActions: bindActionCreators(userinfoActions, dispatch)
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Header)


// reducers/userReducer.js
export default function userinfo(state = {}, action) {
    switch (action.type) {
        case "USERINFO_LOGIN":
            return action.data
        default:
            return state
    }
}


// actions/useraction.js
export function login(data) {
    return {
        type: "USERINFO_LOGIN",
        data
    }
}

上面是一個簡單的場景,進入頁面獲取用戶信息後把用戶信息裏的用戶名顯示在頁面頭部,由於用戶信息多個組件都須要使用,不光頭部組件要用,因此放到redux裏共享。

咱們能夠看到使用react-redux後主要用到裏面的兩個東西,一個是Provider,一個是connect,另外,還須要本身定義兩個函數mapStateToProps, mapDispatchToProps傳給connect,接下來咱們分別來講說這些東西是幹什麼的以及如何實現。

Provider

咱們先來看下Provider,Provider是個高階組件,咱們能夠看到使用它時將包裹在根組件外邊,而且store做爲它的props傳入進去,它的做用就是
將本身做爲全部組件的根組件,而後將store掛載到它的context上讓它下面的全部子組件均可以共享全局狀態。來看下如何實現:

// Provider.js
import React, { Component } from 'react';
import propTypes from 'prop-types';
export default class Provider extends Component {
    static childContextTypes = {
        store: propTypes.object.isRequired
    }
    getChildContext() {
        return { store: this.props.store };
    }
    render() {
        return this.props.children;
    }
}

這個仍是比較好實現的,寫一個組件Provider,將store掛載到Provider的context上,而後使用的時候將Provider包在根組件外邊,由於Provider是原來根組件的父組件,因此它就成了真正的根組件,全部下面的子組件均可以經過context訪問到store,Provider組件利用context的特性解決了項目裏每一個組件都須要import一下store才能使用redux的問題,大大增長了便利性。

connect

首先咱們想一下,只用Provider行不行,固然能夠,由於store已經掛載到根組件上的context,全部子組件均可以經過context訪問到store,而後使用store裏的狀態,而且用store的dispatch提交action更新狀態,可是這樣仍是有些不便利,由於每一個組件都對context依賴過強,形成了組件與store打交道的邏輯和組件自己邏輯都耦合了一塊兒,使得組件沒法複用。

咱們的理想狀態是一個組件的渲染只依賴於外界傳進去的props和本身的state,而並不依賴於其餘的外界的任何數據,這樣的組件複用性是最強的。如何把組件與store打交道的邏輯和組件自身的邏輯分開呢,答案仍是使用高階組件,咱們把原來的寫的業務組件(如header,list等)外邊再包裝一層組件,讓組件與store打交道的部分放在外層組件,內層組件只負責自身的邏輯,外層組件與內層組件經過props進行交流,這樣組件與store打交道的地方就像一層殼同樣與組件實體分開了,咱們能夠將組件實體複用到任何地方只須要換殼便可,connect函數就是負責作上述事情。

示例
學習如何實現connect前先來看下使用connect時須要傳入的參數,mapStateToProps是一個函數。它的做用就是像它的名字那樣,創建一個從(外部的)state對象到(UI 組件的)props對象的映射關係。

mapDispatchToProps是connect函數的第二個參數,用來創建UI組件的參數到store.dispatch方法的映射。也就是說,它定義了用戶的哪些操做應該看成 Action,傳給Store。它能夠是一個函數,也能夠是一個對象。

這兩個函數咱們能夠簡單的理解爲內層組件實體對外層殼組件的要求,組件實體經過mapStateToProps告訴殼組件要store上的哪些狀態,殼組件就去store上拿了之後以props的形式傳給組件實體,mapDispatchToProps同理。

另外咱們在使用connect時通常這樣寫export default connect(mapStateToProps,mapDispatchToProps)(Header),因此connect函數要先接收mapStateToProps, mapDispatchToProps這兩個函數,再返回一個函數,返回的這個函數的參數接收要包裝的組件,最後函數執行返回包好殼的組件。
有朋友可能會問,爲何不直接connect(mapStateToProps,mapDispatchToProps,Header),還得分紅兩個函數來寫,由於React-redux官方就是這麼設計的,我的以爲做者是想提升connect函數的複用性,這裏咱們不去深究它的設計思路,我麼仍是把重心放到它的代碼實現上。

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import propTypes from 'prop-types';
export default function connect(mapStateToProps, mapDispatchToProps) {
    return function(WrapedComponent) {
        //殼組件
        class ProxyComponent extends Component {
            static contextTypes = {
                store: propTypes.object
            }
            constructor(props, context) {
                super(props, context);
                this.store = context.store;
                this.state = mapStateToProps(this.store.getState());
            }

            componentWillMount() {
                this.store.subscribe(() => {
                    this.setState(mapStateToProps(this.store.getState()));
                });
            }
            render() {
                let actions = {};
                if (typeof mapDispatchToProps == 'function') {
                    actions = mapDispatchToProps(this.store.disaptch);
                } else if (typeof mapDispatchToProps == 'object') {
                    actions = bindActionCreators(mapDispatchToProps, this.store.dispatch);
                }
                //殼組件內部渲染真正的組件實體,並將業務組件想要的store裏的狀態及想要觸發的action以props形式傳入
                return <WrapedComponent {...this.state } {...actions}
                />
            }
        }
        return ProxyComponent;
    }
}

咱們來看下connect函數作了什麼?

  1. 首先接收mapStateToProps, mapDispatchToProps並返回一個函數,返回的函數接收一個組件。
  2. 聲明瞭一個殼組件ProxyComponent,並經過context拿到store對象。
  3. 而後在constructor裏經過傳進來的mapStateToProps函數把組件實體想要的狀態經過上一步拿到的store對象裏面的getState方法拿到並存在殼組件的state上。
  4. 在殼組件componentWillMount的生命週期中註冊當store狀態發生變化的回調函數:store變化,同步更新本身的state爲最新的狀態,與store上的狀態保持一致。
  5. 將組件要使用dispatch提交的相關action都封裝成函數。這一步咱們具體展開看下是怎麼作的,首先判斷一下mapDispatchToProps是函數仍是對象,由於咱們在日常使用mapDispatchToProps時通常有兩種常見寫法,一種是在mapDispatchToProps參數位置傳一個函數:
function mapDispatchToProps(dispatch) {
    return {
        userinfoActions: bindActionCreators(userinfoActions, dispatch)
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(Header)

另外一種是直接傳一個action creator對象

export default connect(mapStateToProps, ...userinfoActions )(Header)

咱們要保證不管用戶傳入的mapDispatchToProps是函數仍是action creator對象,咱們都要讓用戶在組件實體內提交action時均可以使用this.props.xxx()的方式去提交,而不用直接接觸store的dispatch方法。

因此,咱們須要藉助redux的bindActionCreators方法,
本系列的第二篇文章 一塊兒學習造輪子(二):從零開始寫一個Redux裏曾經介紹過這個方法的實現原理,這個方法可以讓咱們以方法的形式來提交action,同時,自動dispatch對應的action。因此咱們能夠看到,當用戶傳入的是函數時,用戶在mapDispatchToProps函數內部使用bindActionCreators將action creator轉化成了一個一個的方法,而若是直接傳入action creator對象,那麼咱們在connect內部使用bindActionCreators將傳入的action creator轉化成了一個一個的方法,也就是說假如用戶不作這步操做,那麼react-redux幫你作。

  1. 下一步將殼組件state上的全部屬性及上一步全部已經封裝成函數的action都經過props的方法傳給組件實體。
  2. 最後,把包裝後的組件返回出去,如今咱們在組件實體內部就可使用this.props.username的方式去獲取store上的狀態,或者使用this.props.userinfoActions.login(data)的方式來提交action,此時組件與store打交道的邏輯和組件自身的邏輯分開,內部組件實體能夠進行復用。

最後

本篇介紹了React-Redux的核心實現原理,經過封裝Provider組件和connect方法實現了一個簡單小巧的react-redux,本篇相關代碼都放在github上,能夠點此查看,若是以爲不錯,歡迎star,本系列不按期更新,歡迎關注~

相關文章
相關標籤/搜索