【React系列】動手實現一個react-redux

react-redux 是什麼

react-reduxredux 官方 React 綁定庫。它幫助咱們鏈接UI層和數據層。本文目的不是介紹 react-redux 的使用,而是要動手實現一個簡易的 react-redux,但願可以對你有所幫助。javascript

首先思考一下,假若不使用 react-redux,咱們的 react 項目中該如何結合 redux 進行開發呢。java

每一個須要與 redux 結合使用的組件,咱們都須要作如下幾件事:react

  • 在組件中獲取 store 中的狀態
  • 監聽 store 中狀態的改變,在狀態改變時,刷新組件
  • 在組件卸載時,移除對狀態變化的監聽。

以下:git

import React from 'react';
import store from '../store';
import actions from '../store/actions/counter';
/** * reducer 是 combineReducer({counter, ...}) * state 的結構爲 * { * counter: {number: 0}, * .... * } */
class Counter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            number: store.getState().counter.number
        }
    }
    componentDidMount() {
        this.unsub = store.subscribe(() => {
            if(this.state.number === store.getState().counter.number) {
                return;
           	}
            this.setState({
                number: store.getState().counter.number
            });
        });
    }
    render() {
        return (
            <div>
                <p>{`number: ${this.state.number}`}</p>
                <button onClick={() => {store.dispatch(actions.add(2))}}>+</button>
                <button onClick={() => {store.dispatch(actions.minus(2))}}>-</button>
            <div>
        )
    }
    componentWillUnmount() {
        this.unsub();
    }
}
複製代碼

若是咱們的項目中有不少組件須要與 redux 結合使用,那麼這些組件都須要重複寫這些邏輯。顯然,咱們須要想辦法複用這部分的邏輯,否則會顯得咱們很蠢。咱們知道,react 中高階組件能夠實現邏輯的複用。github

文中所用到的 [Counter 代碼] (github.com/YvetteLau/B…) 中的 myreact-redux/counter 中,建議先 clone 代碼,固然啦,若是以爲本文不錯的話,給個star鼓勵。redux

邏輯複用

src 目錄下新建一個 react-redux 文件夾,後續的文件都新建在此文件夾中。數組

建立 connect.js 文件

文件建立在 react-redux/components 文件夾下:性能優化

咱們將重複的邏輯編寫 connect 中。app

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

export default function connect (WrappedComponent) {
    return class Connect extends Component {
        constructor(props) {
            super(props);
            this.state = store.getState();
        }
        componentDidMount() {
            this.unsub = store.subscribe(() => {
                this.setState({
                    this.setState(store.getState());
                });
            });
        }
        componentWillUnmount() {
            this.unsub();
        }
        render() {
            return (
                <WrappedComponent {...this.state} {...this.props}/> ) } } } 複製代碼

有個小小的問題,儘管這邏輯是重複的,可是每一個組件須要的數據是不同的,不該該把全部的狀態都傳遞給組件,所以咱們但願在調用 connect 時,可以將須要的狀態內容告知 connect。另外,組件中可能還須要修改狀態,那麼也要告訴 connect,它須要派發哪些動做,不然 connect 沒法知道該綁定那些動做給你。ide

爲此,咱們新增兩個參數:mapStateToPropsmapDispatchToProps,這兩個參數負責告訴 connect 組件須要的 state 內容和將要派發的動做。

mapStateToProps 和 mapDispatchToProps

咱們知道 mapStateToPropsmapDispatchToProps 的做用是什麼,可是目前爲止,咱們還不清楚,這兩個參數應該是一個什麼樣的格式傳遞給 connect 去使用。

import { connect } from 'react-redux';
....
//connect 的使用
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
複製代碼
  • mapStateToProps 告訴 connect ,組件須要綁定的狀態。

    mapStateToProps 須要從整個狀態中挑選組件須要的狀態,可是在調用 connect 時,咱們並不能獲取到 store ,不過 connect 內部是能夠獲取到 store 的,爲此,咱們將 mapStateToProps 定義爲一個函數,在 connect 內部調用它,將 store 中的 state 傳遞給它,而後將函數返回的結果做爲屬性傳遞給組件。組件中經過 this.props.XXX 來獲取。所以,mapStateToProps 的格式應該相似下面這樣:

    //將 store.getState() 傳遞給 mapStateToProps
    mapStateToProps = state => ({
        number: state.counter.number
    });
    複製代碼
  • mapDispatchToProps 告訴 connect,組件須要綁定的動做。

    回想一下,組件中派發動做:store.dispatch({actions.add(2)})connect 包裝以後,咱們仍要能派發動做,確定是 this.props.XXX() 這樣的一種格式。

    好比,計數器的增長,調用 this.props.add(2),就是須要派發 store.dispatch({actions.add(2)}),所以 add 屬性,對應的內容就是 (num) => { store.dispatch({actions.add(num)}) }。傳遞給組件的屬性相似下面這樣:

    {
        add: (num) => {
            store.dispatch(actions.add(num))
        },
        minus: (num) => {
            store.dispatch(actions.minus(num))
        }
    }
    複製代碼

    mapStateToProps 同樣,在調用 connect 時,咱們並不能獲取到 store.dispatch,所以咱們也須要將 mapDispatchToProps 設計爲一個函數,在 connect 內部調用,這樣能夠將 store.dispatch 傳遞給它。因此,mapStateToProps 應該是下面這樣的格式:

    //將 store.dispacth 傳遞給 mapDispatchToProps
    mapDispatchToProps = (dispatch) => ({
        add: (num) => {
            dispatch(actions.add(num))
        },
        minus: (num) => {
            dispatch(actions.minus(num))
        }
    })
    複製代碼

至此,咱們已經搞清楚 mapStateToPropsmapDispatchToProps 的格式,是時候進一步改進 connect 了。

connect 1.0 版本

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

export default function connect (mapStateToProps, mapDispatchToProps) {
    return function wrapWithConnect (WrappedComponent) {
        return class Connect extends Component {
            constructor(props) {
                super(props);
                this.state = mapStateToProps(store.getState());
                this.mappedDispatch = mapDispatchToProps(store.dispatch);
            }
            componentDidMount() {
                this.unsub = store.subscribe(() => {
                    const mappedState = mapStateToProps(store.getState());
                    //TODO 作一層淺比較,若是狀態沒有改變,則不setState
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} /> ) } } } } 複製代碼

咱們知道,connect 是做爲 react-redux 庫的方法提供的,所以咱們不可能直接在 connect.js 中去導入 store,這個 store 應該由使用 react-redux 的應用傳入。react 中數據傳遞有兩種:經過屬性 props 或者是經過上下文對象 context,經過 connect 包裝的組件在應用中分佈,而 context 設計目的是爲了共享那些對於一個組件樹而言是「全局」的數據。

咱們須要把 store 放在 context 上,這樣根組件下的全部子孫組件均可以獲取到 store。這部份內容,咱們固然能夠本身在應用中編寫相應代碼,不過很顯然,這些代碼在每一個應用中都是重複的。所以咱們把這部份內容也封裝在 react-redux 內部。

此處,咱們使用舊的 Context API 來寫(鑑於咱們實現的 react-redux 4.x 分支的代碼,所以咱們使用舊版的 context API)。

Provider

咱們須要提供一個 Provider 組件,它的功能就是接收應用傳遞過來的 store,將其掛在 context 上,這樣它的子孫組件就均可以經過上下文對象獲取到 store

新建 Provider.js 文件

文件建立在 react-redux/components 文件夾下:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class Provider extends Component {
    static childContextTypes = {
        store: PropTypes.shape({
            subscribe: PropTypes.func.isRequired,
            dispatch: PropTypes.func.isRequired,
            getState: PropTypes.func.isRequired
        }).isRequired
    }
    
    constructor(props) {
        super(props);
        this.store = props.store;
    }

    getChildContext() {
        return {
            store: this.store
        }
    }

    render() {
        /** * 早前返回的是 return Children.only(this.props.children) * 致使Provider只能包裹一個子組件,後來取消了此限制 * 所以此處,咱們直接返回 this.props.children */
        return this.props.children
    }
}
複製代碼
新建一個 index.js 文件

文件建立在 react-redux 目錄下:

此文件只作一件事,即將 connectProvider 導出

import connect from './components/connect';
import Provider from './components/Provider';

export {
    connect,
    Provider
}
複製代碼

Provider 的使用

使用時,咱們只須要引入 Provider,將 store 傳遞給 Provider

import React, { Component } from 'react';
import { Provider } from '../react-redux';
import store from './store';
import Counter from './Counter';

export default class App extends Component {
    render() {
        return (
            <Provider store={store}> <Counter /> </Provider>
        )
    }
}
複製代碼

至此,Provider 的源碼和使用已經說明清楚了,不過相應的 connect 也須要作一些修改,爲了通用性,咱們須要從 context 上去獲取 store,取代以前的導入。

connect 2.0 版本

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default function connect(mapStateToProps, mapDispatchToProps) {
    return function wrapWithConnect(WrappedComponent) {
        return class Connect extends Component {
            //PropTypes.shape 這部分代碼與 Provider 中重複,所以後面咱們能夠提取出來
            static contextTypes = {
                store: PropTypes.shape({
                    subscribe: PropTypes.func.isRequired,
                    dispatch: PropTypes.func.isRequired,
                    getState: PropTypes.func.isRequired
                }).isRequired
            }

            constructor(props, context) {
                super(props, context);
                this.store = context.store;
                //源碼中是將 store.getState() 給了 this.state
                this.state = mapStateToProps(this.store.getState());
                this.mappedDispatch = mapDispatchToProps(this.store.dispatch);
            }
            componentDidMount() {
                this.unsub = this.store.subscribe(() => {
                    const mappedState = mapStateToProps(this.store.getState());
                    //TODO 作一層淺比較,若是狀態沒有改變,則無需 setState
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} /> ) } } } } 複製代碼

使用 connect 關聯 Counterstore 中的數據。

import React, { Component } from 'react';
import { connect } from '../react-redux';
import actions from '../store/actions/counter';

class Counter extends Component {
    render() {
        return (
            <div> <p>{`number: ${this.props.number}`}</p> <button onClick={() => { this.props.add(2) }}>+</button> <button onClick={() => { this.props.minus(2) }}>-</button> </div>
        )
    }
}

const mapStateToProps = state => ({
    number: state.counter.number
});

const mapDispatchToProps = (dispatch) => ({
    add: (num) => {
        dispatch(actions.add(num))
    },
    minus: (num) => {
        dispatch(actions.minus(num))
    }
});


export default connect(mapStateToProps, mapDispatchToProps)(Counter);
複製代碼

store/actions/counter.js 定義以下:

import { INCREMENT, DECREMENT } from '../action-types';

const counter = {
    add(number) {
        return {
            type: INCREMENT,
            number
        }
    },
    minus(number) {
        return {
            type: DECREMENT,
            number
        }
    }
}
export default counter;
複製代碼

至此,咱們的 react-redux 庫已經可使用了,不過頗有不少細節問題待處理:

  • mapDispatchToProps 的定義寫起來有點麻煩,不夠簡潔 你們是否還記得 redux 中的 bindActionCreators,藉助於此方法,咱們能夠容許傳遞 actionCreatorconnect,而後在 connect 內部進行轉換。

  • connectProvider 中的 storePropType 規則能夠提取出來,避免代碼的冗餘

  • mapStateToPropsmapDispatchToProps 能夠提供默認值 mapStateToProps 默認值爲 state => ({}); 不關聯 state

    mapDispatchToProps 的默認值爲 dispatch => ({dispatch}),將 store.dispatch 方法做爲屬性傳遞給被包裝的屬性。

  • 目前,咱們僅傳遞了 store.getState()mapStateToProps,可是極可能在篩選過濾須要的 state 時,須要依據組件自身的屬性進行處理,所以,能夠將組件自身的屬性也傳遞給 mapStateToProps,一樣的緣由,也將自身屬性傳遞給 mapDispatchToProps

connect 3.0 版本

咱們將 store 的 PropType 規則提取出來,放在 utils/storeShape.js 文件中。

淺比較的代碼放在 utils/shallowEqual.js 文件中,通用的淺比較函數,此處不列出,有興趣能夠直接閱讀下代碼。

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/** * mapStateToProps 默認不關聯state * mapDispatchToProps 默認值爲 dispatch => ({dispatch}),將 `store.dispatch` 方法做爲屬性傳遞給組件 */
const defaultMapStateToProps = state => ({});
const defaultMapDispatchToProps = dispatch => ({ dispatch });

export default function connect(mapStateToProps, mapDispatchToProps) {
    if(!mapStateToProps) {
        mapStateToProps = defaultMapStateToProps;
    }
    if (!mapDispatchToProps) {
        //當 mapDispatchToProps 爲 null/undefined/false...時,使用默認值
        mapDispatchToProps = defaultMapDispatchToProps;
    }
    return function wrapWithConnect(WrappedComponent) {
        return class Connect extends Component {
            static contextTypes = {
                store: storeShape
            };
            constructor(props, context) {
                super(props, context);
                this.store = context.store;
                //源碼中是將 store.getState() 給了 this.state
                this.state = mapStateToProps(this.store.getState(), this.props);
                if (typeof mapDispatchToProps === 'function') {
                    this.mappedDispatch = mapDispatchToProps(this.store.dispatch, this.props);
                } else {
                    //傳遞了一個 actionCreator 對象過來
                    this.mappedDispatch = bindActionCreators(mapDispatchToProps, this.store.dispatch);
                }
            }
            componentDidMount() {
                this.unsub = this.store.subscribe(() => {
                    const mappedState = mapStateToProps(this.store.getState(), this.props);
                    if (shallowEqual(this.state, mappedState)) {
                        return;
                    }
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} /> ) } } } } 複製代碼

如今,咱們的 connect 容許 mapDispatchToProps 是一個函數或者是 actionCreators 對象,在 mapStateToPropsmapDispatchToProps 缺省或者是 null 時,也能表現良好。

不過還有一個問題,connect 返回的全部組件名都是 Connect,不便於調試。所以咱們能夠爲其新增 displayName

connect 4.0 版本

import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import storeShape from '../utils/storeShape';
import shallowEqual from '../utils/shallowEqual';
/** * mapStateToProps 缺省時,不關聯state * mapDispatchToProps 缺省時,設置其默認值爲 dispatch => ({dispatch}),將`store.dispatch` 方法做爲屬性傳遞給組件 */ 
const defaultMapStateToProps = state => ({});
const defaultMapDispatchToProps = dispatch => ({ dispatch });

function getDisplayName(WrappedComponent) {
    return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

export default function connect(mapStateToProps, mapDispatchToProps) {
    if(!mapStateToProps) {
        mapStateToProps = defaultMapStateToProps;
    }
    if(!mapDispatchToProps) {
        //當 mapDispatchToProps 爲 null/undefined/false...時,使用默認值
        mapDispatchToProps = defaultMapDispatchToProps;
    }
    return function wrapWithConnect (WrappedComponent) {
        return class Connect extends Component {
            static contextTypes = storeShape;
            static displayName = `Connect(${getDisplayName(WrappedComponent)})`;
            constructor(props) {
                super(props);
                //源碼中是將 store.getState() 給了 this.state
                this.state = mapStateToProps(store.getState(), this.props);
                if(typeof mapDispatchToProps === 'function') {
                    this.mappedDispatch = mapDispatchToProps(store.dispatch, this.props);
                }else{
                    //傳遞了一個 actionCreator 對象過來
                    this.mappedDispatch = bindActionCreators(mapDispatchToProps, store.dispatch);
                }
            }
            componentDidMount() {
                this.unsub = store.subscribe(() => {
                    const mappedState = mapStateToProps(store.getState(), this.props);
                    if(shallowEqual(this.state, mappedState)) {
                        return;
                    }
                    this.setState(mappedState);
                });
            }
            componentWillUnmount() {
                this.unsub();
            }
            render() {
                return (
                    <WrappedComponent {...this.props} {...this.state} {...this.mappedDispatch} /> ) } } } } 複製代碼

至此,react-redux 咱們就基本實現了,不過這個代碼並不完善,好比,ref 丟失的問題,組件的 props 變化時,從新計算 this.statethis.mappedDispatch,沒有進一步進行性能優化等。你能夠在此基礎上進一步進行處理。

react-redux 主幹分支的代碼已經使用 hooks 改寫,後期若是有時間,會輸出一篇新版本的代碼解析。

最後,使用咱們本身編寫的 react-reduxredux 編寫了 Todo 的demo,功能正常,代碼在 在 https://github.com/YvetteLau/Blog 中的 myreact-redux/todo 下。

附上新老 context API 的使用方法:

context

目前有兩個版本的 context API,舊的 API 將會在全部 16.x 版本中獲得支持,可是將來版本中會被移除。

context API(新)

const MyContext = React.createContext(defaultValue);
複製代碼

建立一個 Context 對象。當 React 渲染一個訂閱了這個 Context 對象的組件,這個組件會從組件樹中離自身最近的那個匹配的 Provider 中讀取到當前的 context 值。

注意:只有當組件所處的樹中沒有匹配到 Provider 時,其 defaultValue 參數纔會生效。

使用
Context.js

首先建立 Context 對象

import React from 'react';

const MyContext = React.createContext(null);

export default MyContext;
複製代碼
根組件( Pannel.js )
  • 將須要共享的內容,設置在 <MyContext.Provider>value 中(即 context 值)
  • 子組件被 <MyContext.Provider> 包裹
import React from 'react';
import MyContext from './Context';
import Content from './Content';

class Pannel extends React.Component {
    state = {
        theme: {
            color: 'rgb(0, 51, 254)'
        }
    }
    render() {
        return (
            // 屬性名必須叫 value
            <MyContext.Provider value={this.state.theme}>
                <Content /> </MyContext.Provider> ) } } 複製代碼
子孫組件( Content.js )

類組件

  • 定義 Class.contextType: static contextType = ThemeContext;
  • 經過 this.context 獲取 <ThemeContext.Provider>value 的內容(即 context 值)
//類組件
import React from 'react';
import ThemeContext from './Context';

class Content extends React.Component {
    //定義了 contextType 以後,就能夠經過 this.context 獲取 ThemeContext.Provider value 中的內容
    static contextType = ThemeContext;
    render() {
        return (
            <div style={{color: `2px solid ${this.context.color}`}}> //.... </div>
        )
    }
}
複製代碼

函數組件

  • 子元素包裹在 <ThemeContext.Consumer>
  • <ThemeContext.Consumer> 的子元素是一個函數,入參 context 值(Provider 提供的 value)。此處是 {color: XXX}
import React from 'react';
import ThemeContext from './Context';

export default function Content() {
    return (
        <ThemeContext.Consumer> { context => ( <div style={{color: `2px solid ${context.color}`}}> //.... </div> ) } </ThemeContext.Consumer> ) } 複製代碼

context API(舊)

使用
  • 定義根組件的 childContextTypes (驗證 getChildContext 返回的類型)
  • 定義 getChildContext 方法
根組件( Pannel.js )
import React from 'react';
import PropTypes from 'prop-types';
import Content from './Content';

class Pannel extends React.Component {
    static childContextTypes = {
        theme: PropTypes.object
    }
    getChildContext() {
        return { theme: this.state.theme }
    }
    state = {
        theme: {
            color: 'rgb(0, 51, 254)'
        }
    }
    render() {
        return (
            // 屬性名必須叫 value
            <>
                <Content /> </> ) } } 複製代碼
子孫組件( Content.js )
  • 定義子孫組件的 contextTypes (聲明和驗證須要獲取的狀態的類型)
  • 經過 this.context 便可以獲取傳遞過來的上下文內容。
import React from 'react';
import PropTypes from 'prop-types';

class Content extends React.Component {
    static contextTypes = PropTypes.object;
    render() {
        return (
            <div style={{color: `2px solid ${this.context.color}`}}> //.... </div>
        )
    }
}
複製代碼

參考連接:


關注公衆號,加入技術交流羣

相關文章
相關標籤/搜索