【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>
        )
    }
}
复制代码

参考连接:


关注公众号,加入技术交流群

相关文章
相关标签/搜索