简单的例子理解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源码的解析。

相关文章
相关标签/搜索