簡單的例子理解react-redux原理及實現

本文會經過一個簡單的例子講解react-redux的使用及其原理,而且該例子會用redux和react-redux都寫一遍,對比其不一樣來加深理解。css

咱們要實現的例子很簡單,就是頁面上有一個按鈕和一個數字,點擊add按鈕的時候,數字會加1。雖然是個極簡的例子,但已經包括了數據的響應化和事件交互這兩個最核心的功能。html

實現的例子頁面截圖以下:react

redux

首先咱們來看下redux是如何實現該功能的(redux的原理及實現能夠看上篇文章):redux

首先經過redux提供的createStore建立一個store實例,createStore接收一個reducer函數做爲參數。代碼實現以下:閉包

// store.js
import {createStore} from 'redux';

function countReducer(state=0, action) {
    if (action.type === 'ADD') {
        return state + 1;
    }
    return state;
}

const store = createStore(countReducer);

export default store;
複製代碼

store提供了三個方法供外部調用,分別是dispatch、subscribe和getState。app

  • dispatch:經過調用dispatch(action)告訴store要執行什麼行爲,store會調用reducer函數執行具體的行爲更新state。以上面的例子舉例,當咱們調用store.dispatch({action: 'ADD'}),那麼store就會調用countReducer函數執行,返回的值爲新的state,那麼如今的state就+1了。
  • subscribe:該方法提供的是訂閱功能,其參數是一個函數,其訂閱的是state變化這個行爲,就是說每當state變化的時候,就會執行訂閱的函數。例如當咱們調用store.subscribe(fn);那麼當store裏的state變化的時候,fn就會被執行。
  • getState:獲取當前的state,咱們能夠經過調用store.state()來獲取當前最新的state。

當心得:dispatch和subscribe其實能夠理解爲就是一種發佈訂閱模式。框架

當咱們寫好store後,就能夠寫咱們的業務組件了,代碼以下:dom

import React, {Component} from 'react';
import store from '../store'; // 引入store

export default class ReactReduxPage extends Component {
    componentDidMount () {
        // 在組件掛在的時候訂閱state的變化
        store.subscribe(() => {
            this.forceUpdate(); // 每當state變化的時候就從新渲染組件
        })
    }

    // 當點擊按鈕的時候,dispatch一個ADD action,觸發store裏的reducer方法更新state
    add () {
        store.dispatch({
            type: 'ADD'
        })
    }

    render () {
        return (
            <div> <div>number: {store.getState()}</div> <button onClick={this.add}>add</button> </div>
        )
    }
}
複製代碼

以上就是用redux實現該例子的全部代碼,使用redux咱們須要本身調用store.subscribe、store.dispatch和store.getState這些方法,而且在state更新的時候,要本身從新觸發render,業務邏輯複雜的話仍是會有一點麻煩,因此redux的做者封裝了一個專門給react使用的redux模塊,就是react-redux。ide

react-redux

react-redux其實就是對redux進行了再一步的封裝,實際底層仍是使用的redux。咱們先用react-redux來實現本文的小例子來看下react-redux是怎麼用的。而後再經過本身實現一個react-redux框架來看下redux-react是怎麼對redux進行再一步的封裝的。函數

react-redux實現計數器

咱們用react-redux實現和上面同樣的例子:

Provider

react-redux提供了一個Provider組件,該組件能夠將store提供給全部的子組件使用,其利用的是react的context屬性,具體使用方法以下:

// 入口文件index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';


// ========= 重點 =========
import {Provider} from "react-redux";
import store from './store';
// ========= 重點 =========



ReactDOM.render(
  // 在App組件外包一層Provider
  <Provider store={store}>
    <App /> </Provider>,
  document.getElementById('root')
);
複製代碼

如上重點就是咱們在入口文件index.js引入Providerstore;而後利用Providerstore注入到全部的子組件中,那麼全部的子組件就均可以拿到store了。該特性利用的是react的context屬性,對context不瞭解的能夠去看下react文檔,這裏再也不贅述。

store

store.js的代碼和上面的redux是同樣的,這裏就不重複寫一遍了。

connect

而後咱們開始寫咱們的業務組件,咱們的業務組件如今變成了以下所示:

import React, {Component} from 'react';
import {connect} from 'react-redux';

// 重點!使用了connect高階組件
// connect使用方式爲 connect(mapStateToProps, mapDispatchToProps)(originalComponent)
// 執行 connect(mapStateToProps, mapDispatchToProps)(originalComponent)後會返回一個新的組件
export default connect(
    state => ({count: state}),
    dispatch => ({add: () => dispatch({type: 'ADD'})})
)(
    class ReactReduxPage extends Component {
        render () {
            const {count, add} = this.props
            console.error('this.props:', this.props);
            return (
                <div> <div>number: {count}</div> <button onClick={add}>add</button> </div>
            )
        }
    }
)
複製代碼

如上所示,咱們使用react-redux寫組件的時候,組件的屬性和方法都要經過props獲取。例如上面例子的this.props.count以及this.props.add。props裏的屬性和方法是哪裏來的呢?答案是connect函數放進去的,connect接收兩個函數做爲參數,分別是mapStateToProps和mapDispatchToProps,以上面的例子爲例分別對應以下:

mapStateToProps函數: state => ({count: state})
mapDispatchToProps函數: dispatch => ({add: () => dispatch({type: 'ADD'})})
複製代碼

這兩個函數都會返回一個對象,對象裏面的鍵值對都會做爲props裏的鍵值對,其原理以下:

mapStateToProps函數執行返回 obj1 : {count: state}
mapDispatchToProps函數執行返回 obj2 :{add: () => dispatch({type: 'ADD'})}

那麼最後提供給組件使用的props就是:
{
    ...obj1,
    ...obj2
}
也就是
{
    count: state,
    add: () => dispatch({type: 'ADD'}
}
複製代碼

當咱們點擊add按鈕的時候,就會調用this.props.add方法,add方法會調用dispatch({type: 'ADD'}),而後就會執行store裏的reducer方法更新state,更新state後react-redux就會從新幫咱們渲染組件,下面咱們經過本身實現react-redux來看下這個流程是怎麼運行的。

react-redux 源碼實現

從上面的代碼中咱們能夠看到,react-redux給外面提供了兩個方法,分別是Providerconnect,因此react-redux內的代碼結構應該是這樣的:

// react-redux.js
import React, {Component} from 'react';

export const connect = (
    mapStateToProps,
    mapDispatchToProps
) => WrappedComponent => {
    // 執行函數體內代碼邏輯 
    // return 一個組件
}

export class Provider extends Component {
    render () {
        // return ...
    }
}
複製代碼

Provider源碼實現

Provider的做用是向下提供store,讓組件樹內的全部組件都能獲取到store實例。咱們先回顧一下主入口文件index.js是如何使用Provider向下提供store的:

// 入口文件index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';


// ========= 重點 =========
import {Provider} from "react-redux";
import store from './store';
// ========= 重點 =========



ReactDOM.render(
  // 在App組件外包一層Provider
  <Provider store={store}>
    <App /> </Provider>,
  document.getElementById('root')
);
複製代碼

而後咱們再來本身實現react-redux裏的Provider:

// react-redux.js
import React, {Component} from 'react';

// 建立一個上下文,名字隨便取
// Context 可讓咱們無須明確地傳遍每個組件,就能將值深刻傳遞進組件樹。
const StoreContext = React.createContext();

// 重點!Provider的實現
export class Provider extends Component {
    render () {
        // 使用一個 Provider 來將當前的 store 傳遞給如下的組件樹。
        // 不管多深,任何組件都能讀取這個值。
        return (
           <StoreContext.Provider value={this.props.store}> {this.props.children} </StoreContext.Provider> ) } } 複製代碼

從上面的代碼能夠看到,Provider是使用react的context來實現的,context的做用能夠查看官方文檔:

react.docschina.org/docs/contex…

咱們經過建立一個上下文StoreContext,而後用StoreContext.Provider向組件樹向下傳遞store。組件樹裏的組件經過static contextType = StoreContext;,組件內的this.context就指向StoreContext.Provider提供的value裏的值了,也就是store實例。context就再也不過多贅述了,看下官方文檔的用法,上面的代碼天然就能理解了。

connect源碼實現

connect 是一個雙箭頭函數,最終返回的是一個組件。

首先執行 connect(mapStateToProps,mapDispatchToProps)返回一個函數,而後該函數再接收一個組件做爲參數,最後返回一個新的組件,這是高階組件的用法。

因此咱們使用connect的方式是這樣的connect(mapStateToProps,mapDispatchToProps)(WrappedComponent),WrappedComponent是一個純組件,它只接收props,經過props來渲染出組件的內容,內部不保存狀態。props經過mapStateToPropsmapDispatchToProps這兩個函數提供。react-redux這麼設計的緣由就是爲了讓WrappedComponent只負責接收props數據和渲染組件,不用關心狀態,給它什麼就顯示什麼,因此WrappedComponent組件的邏輯會簡單清晰一些,而且組件的職責也更加明確。而負責數據管理和邏輯的這些操做都放在執行 connect(mapStateToProps,mapDispatchToProps)(WrappedComponent) 後返回的這個組件裏完成,這裏會比較繞,後面會經過寫代碼詳細講解。

當咱們執行connect(mapStateToProps,mapDispatchToProps)(WrappedComponent)的時候,函數體內就可使用mapStateToPropsmapDispatchToPropsWrappedComponent這幾個參數了。connect函數的做用就是返回一個新的封裝過的組件,而封裝這個組件時候須要用到mapStateToPropsmapDispatchToPropsWrappedComponent這幾個參數來處理一些邏輯。

tips小思考:connect不用雙箭頭函數可不能夠呢?也是能夠的,由於connet函數最終是要返回一個新的組件,而在封裝這個新的組件的過程當中須要用到mapStateToPropsmapDispatchToPropsWrappedComponent來作一些事情,不用雙箭頭也能夠達到這個效果,咱們像下面這樣寫也能實現同樣的功能:

export const connect = (
    mapStateToProps,
    mapDispatchToProps,
    WrappedComponent
) => {
    // 執行函數體內代碼邏輯 
    // return 一個組件
}

而後使用的時候用
connect(
    mapStateToProps,
    mapDispatchToProps,
    WrappedComponent
)
不須要使用
connect(
    mapStateToProps,
    mapDispatchToProps
)(WrappedComponent)
複製代碼

至於react-redux官方爲何用雙箭頭呢,我以爲主要是有如下幾個優勢:

  • 一、參數職責劃分明確,第一個箭頭函數的參數就是用來映射props的,第二個箭頭函數的參數就是用來傳要被封裝的組件的
  • 二、逼格比較高,夠騷氣,充分利用了閉包和函數做用域的原理

咱們先實現一個能渲染出WrappedComponent組件內容的版本,主要看標註了重點的部分,由於connect返回的組件在Provider內部,因此能夠拿到store實例,因此就能調用store的getState方法拿到最新state。而後咱們調用mapStateToProps(state),就能獲得要傳遞給WrappedComponent的stateProps了。

export const connect = (
    mapStateToProps,
    mapDispatchToProps
) => WrappedComponent => {
    return class extends Component {
        // 指定 contextType 讀取當前的 store context。
        // React 會往上找到最近的 store Provider,而後使用它的值。
        static contextType = StoreContext;
        constructor(props) {
            super(props);
            this.state = {
              props: {}
            };
        }


        // ============== 重點 ==============
        componentDidMount () {
            this.update();
        }

        update () {
            // this.context已經指向咱們的store實例了
            const {dispatch, subscribe, getState} = this.context;

            // 調用mapStateToProps,並將store最新的state做爲參數
            // 返回咱們要傳遞給WrappedComponent的props
            let stateProps = mapStateToProps(getState());

            // 調用setState觸發render,更新WrappedComponent內容
            this.setState({
                props: {
                    ...stateProps
                }
            })
        }

        render() {
            return <WrappedComponent {...this.state.props}/> } // ============== 重點 ============== } } 複製代碼

上面的代碼已經能顯示咱們的初始化界面了,可是如今還不能處理事件,因此咱們須要使用mapDispatchToProps生成事件到props的映射,再來回顧下這兩個函數:

mapStateToProps函數: state => ({count: state})
mapDispatchToProps函數: dispatch => ({add: () => dispatch({type: 'ADD'})})
複製代碼

增長了對事件處理的代碼以下,新增的代碼就兩行,就是調用mapDispatchToProps生成事件相關的props傳遞給WrappedComponent組件。

update () {
    // this.context已經指向咱們的store實例了
    const {dispatch, getState} = this.context;

    // 調用mapStateToProps,並將store最新的state做爲參數
    // 返回咱們要傳遞給WrappedComponent的props
    let stateProps = mapStateToProps(getState());

    // 調用mapDispatchToProps返回事件相關的props
    let dispatchProps = mapDispatchToProps(dispatch); // !重點新加代碼

    // 調用setState觸發render,更新WrappedComponent內容
    this.setState({
        props: {
            ...stateProps,
            ...dispatchProps // !重點新加代碼
        }
    })
}
複製代碼

state變化的時候須要從新渲染組件,因此咱們還須要增長一個訂閱功能:

componentDidMount () {
    this.update();

    // ===== 新加代碼 =====
    const {subscribe} = this.context;
    // state變化的時候從新渲染組件
    subscribe (() => {
        this.update() 
    })
    // ===== 新加代碼 =====
}
複製代碼

本身實現的react-redux完整代碼:

// react-redux簡易源碼
import React, {Component} from 'react';

// 建立一個上下文,名字隨便取
// Context 可讓咱們無須明確地傳遍每個組件,就能將值深刻傳遞進組件樹。
const StoreContext = React.createContext();

export const connect = (
    mapStateToProps,
    mapDispatchToProps
) => WrappedComponent => {
    return class extends Component {
        // 指定 contextType 讀取當前的 store context。
        // React 會往上找到最近的 store Provider,而後使用它的值。
        static contextType = StoreContext;
        constructor(props) {
            super(props);
            this.state = {
              props: {}
            };
        }


        // ============== 重點 ==============
        componentDidMount () {
            this.update();

            const {subscribe} = this.context;
            // state變化的時候從新渲染組件
            subscribe (() => {
                this.update()
            })
        }

        update () {
            // this.context已經指向咱們的store實例了
            const {dispatch, getState} = this.context;

            // 調用mapStateToProps,並將store最新的state做爲參數
            // 返回咱們要傳遞給WrappedComponent的props
            let stateProps = mapStateToProps(getState());

            // 調用mapDispatchToProps返回事件相關的props
            let dispatchProps = mapDispatchToProps(dispatch); // !重點新加代碼

            // 調用setState觸發render,更新WrappedComponent內容
            this.setState({
                props: {
                    ...stateProps,
                    ...dispatchProps // !重點新加代碼
                }
            })
        }

        render() {
            return <WrappedComponent  {...this.state.props}/>
        }
        // ============== 重點 ==============
    }
}

export class Provider extends Component {
    render () {
        // 使用一個 Provider 來將當前的 store 傳遞給如下的組件樹。
        // 不管多深,任何組件都能讀取這個值。
        return (
           <StoreContext.Provider value={this.props.store}>
               {this.props.children}
           </StoreContext.Provider> 
        )
    }
}
複製代碼

另:mapDispatchToProps也能夠爲對象,這裏爲了演示方便就再也不講解了,只介紹爲funtion的狀況

業務組件的代碼也貼一下

// 業務組件代碼
import React, {Component} from 'react';
import {connect} from 'react-redux';

// 重點!使用了connect高階組件
// connect使用方式爲 connect(mapStateToProps, mapDispatchToProps)(originalComponent)
// 執行 connect(mapStateToProps, mapDispatchToProps)(originalComponent)後會返回一個新的組件
export default connect(
    state => ({count: state}),
    dispatch => ({add: () => dispatch({type: 'ADD'})})
)(
    class ReactReduxPage extends Component {
        render () {
            const {count, add} = this.props
            console.error('this.props:', this.props);
            return (
                <div> <div>number: {count}</div> <button onClick={add}>add</button> </div>
            )
        }
    }
)
複製代碼

入口js代碼:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';


// ========= 重點 =========
import {Provider} from "react-redux";
import store from './store';
// ========= 重點 =========



ReactDOM.render(
  // 在App組件外包一層Provider
  <Provider store={store}>
    <App /> </Provider>,
  document.getElementById('root')
);
複製代碼

以上就是react-redux源碼的解析。

相關文章
相關標籤/搜索