一起學習造輪子(三):從零開始寫一個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~)

前言

上一章我們寫了一個redux,當redux與react結合時一般為了方便會使用react-redux,
這個庫是可以選用的。實際項目中,應該權衡一下,是直接使用 Redux,還是使用 React-Redux。后者雖然提供了便利,但是需要掌握額外的 API,並且要遵守它的組件拆分規范。
本文對於react-redux的用法不會過多介紹,重點仍然放在源碼實現上。如果還不太了解如何使用,可以看相關文章學習。

推薦文章:

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

本文所有代碼在github建有代碼倉庫,可以點此查看本文代碼,也歡迎大家star~

開始

context

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

例如有這么一棵組件樹:

props傳遞

userinfo用戶信息這個數據是很多組件都需要用的,所以我們按照正常的思路在根節點的 Index 上獲取,然后把這個狀態通過 props一層層傳遞下去,最終所有組件都拿到了userinfo,進行使用。
但是這樣有個問題:

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

我們想,如果這顆組件樹能夠全局共享這個一個狀態倉庫就好了,我們要的時候就去狀態倉庫里取對應的狀態,不用手動地傳,這該多好啊。
全局狀態
React.js 的 context 就是這么一個東西,某個組件只要往自己的 context 里面放了某些狀態,這個組件之下的所有子組件都直接訪問這個狀態而不需要通過中間組件的傳遞,來看下具體怎么用:

//在根組件上將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,本系列不定期更新,歡迎關注~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM