Skip to content

redux学习

理解纯函数

什么是副作用?

在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量修改参数或者改变外部的存储

副作用往往是产生bug的 “温床”。

纯函数的优势

  • 因为你可以安心的编写和安心的使用(不会产生额外的副作用);

  • 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;(解耦

  • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出

react中对组件的要求

React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样保护它们的props不被修改

ReduxReducer也被要求是一个纯函数

为什么需要redux

redux基本概念

redux核心理念包括:storeactionreducer

store

  • store:可以理解为仓库的意思,建立store的目的是使用统一的规范来管理、操作数据数据,以达到跟踪数据变化的目的

action

reducer

redux的三大原则

redux基本使用

简单概括:

  • store是统一的数据仓库,可以向其中添加多个数据(state),可以通过store.getState来获取当前的state

  • 我们只能通过action(修改的动作)来修改store的数据,这样可以记录数据变化,通常action中都会有type属性,也可以携带其他的数据,通过dispatch来派发(触发)action

  • 创建store时必须创建reducerreducer是一个纯函数,接收参数是stateaction,通过reducer将旧stateactions联系在一起,并且返回一个新的State

  • 完成数据更新

代码示例:

redux工具函数

js
const {createStore} = require("redux")

// 初始化数据
const initialState = {
    name: "why",
    count: 100
}

// 定义reducer函数,要求是一个纯函数
// 接收两个参数
// 参数一:store中保存的state
// 参数二:更次需要更新的action(dispatch传入的action)
// 返回值:它的返回值会作为store之后存储的state
// 每次数据变化的时候都会重新执行一次reducer
const reducer = (state = initialState, action) => {
    if (action.type === 'change_name') {
        // 这里很关键,这里赋值用到了浅拷贝,创建了一个新的对象
        // 并没有直接修改state,避免了副作用,符合纯函数的要求
        return {...state, name: action.name}
    } else if (action.type === 'add_count') {
        return {...state, count: state.count + action.count}
    }
    return state
}

// 创建store
const store = createStore(reducer)

module.exports = store

使用示例:

js
const store = require("../index")

console.log("store", store.getState()) // store { name: 'why', count: 100 }

// 修改name的action
const action = {type: "change_name", name: "zhangsan"}
store.dispatch(action)

// 修改count的action
const action2 = {type: "add_count", count: 10}
store.dispatch(action2)

console.log("更新后的state", store.getState()) //更新后的state { name: 'zhangsan', count: 110 }

订阅Store数据变化

通过subscribe可以订阅store中的数据变化

订阅数据变化

js
const store = require("../index")

store.subscribe(() => {
    console.log("subscribe state", store.getState())
})

取消订阅数据变化

js
const store = require("../index")

// store.subscribe的返回值就是取消订阅函数
const unsubscribe= store.subscribe(() => {
    console.log("subscribe state", store.getState())
})

unsubscribe()

React中一般是在componentDidMount中订阅数据变化,然后在componentWillUnmount中取消订阅

redux结构划分

上面已经简单体验了Redux,但在实际项目中,我们会对Redux进行模块化划分

接下来结合实际开发对代码进行优化

1、将action的生成过程统一放到actionCreator.js单独的文件中,dispatch调用时只需要传入值即可

2、将actionCreatorreducer对应的字符串是一致的可以抽离到常量中,避免编写时不一致

3、将reducerinitialState放到一个独立文件夹reducer.js

actionCreator.js

js
const {ADD_NUMBER, CHANGE_NAME} = require("./constants");
const changeNameAction = (name) => ({
    type: CHANGE_NAME,
    name
})

const addNumberAction = (count) => ({
    type: ADD_NUMBER,
    count
})

module.exports = {changeNameAction, addNumberAction}

constants.js

js
const ADD_NUMBER = 'add_count';
const CHANGE_NAME = 'change_name';

module.exports = {
    ADD_NUMBER,
    CHANGE_NAME,
}

reducer.js

js
// 初始化数据
const {CHANGE_NAME, ADD_NUMBER} = require("./constants");

// 初始化state
const initialState = {
    name: "why",
    count: 100
}

const reducer = (state = initialState, action) => {

    switch (action.type) {
        case CHANGE_NAME:
            return {...state, name: action.name}
        case ADD_NUMBER:
            return {...state, count: state.count + action.count}
        default:
            return state
    }
}

module.exports = reducer

index.js

js
const {createStore} = require("redux")

const reducer = require("./reducer");

// 创建store
const store = createStore(reducer)

module.exports = store

使用示例:

js
const store = require("../src/01_使用state中的数据")
const {addNumberAction, changeNameAction} = require("./01_使用state中的数据/actionCreator");

console.log("store", store.getState())

store.dispatch(changeNameAction("zhangsan"))

store.dispatch(addNumberAction(10))

console.log("更新后的state", store.getState())

redux使用流程图

react项目中使用redux

首先封装store相关函数 store/index.js

js
import {createStore} from "redux"
import reducer from "./reducer";

const store = createStore(reducer)

export default store

store/actionCreator.js

js
const {CHANGE_COUNT} = require("./constants");

export const changeCount = (count) => ({
    type: CHANGE_COUNT,
    count
})

store/constants.js

js
export const CHANGE_COUNT = 'change_count';

store/reducer.js

js
import {CHANGE_COUNT} from "./constants";

const initialState = {
    count: 100
}

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case CHANGE_COUNT:
            return {...state, count: action.count}
        default:
            return state
    }
}
export default reducer

使用示例: App.jsx

jsx
import React from "react"
import Home from "./pages/home"
import Profile from "./pages/profile"
import store from "./store"

export default class App extends React.PureComponent {
    constructor() {
        super()
        this.state = {
            count: store.getState().count,
        }
    }

    componentDidMount() {
        store.subscribe(() => {
            const state = store.getState()
            this.setState({
                count: state.count
            })
        })
    }

    render() {
        const {count} = this.state
        return (
            <div>
                <h2>
                    App Counter: {count}
                </h2>
                <div style={{border: '1px solid red'}}>
                    <Home/>
                    <Profile/>
                </div>
            </div>
        )
    }

}

home.jsx

jsx
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import store from "../store";
import {changeCount} from "../store/actionCreator";

let unsubscribe = null
class Home extends Component {
    constructor() {
        super()
        this.state = {
            count: store.getState().count,
        }
    }

    componentDidMount() {
        unsubscribe = store.subscribe(() => {
            console.log("变化:", store.getState())
            const state = store.getState()
            this.setState({
                count: state.count
            })
        })
    }

    componentWillUnmount() {
        unsubscribe()
    }

    changeCount(count) {
        store.dispatch(changeCount(count))
    }

    render() {
        return (
            <div>
                <h2>home</h2>
                <span>当前计数:{this.state.count}</span>
                <button onClick={() => {
                    this.changeCount(this.state.count + 1)
                }}>+1
                </button>
                <button onClick={() => {
                    this.changeCount(this.state.count + 5)
                }}>+5
                </button>
            </div>
        );
    }
}
Home.propTypes = {};
export default Home;

react-redux

安装:

yarn add react-redux

index.jsx

jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import {Provider} from "react-redux"
import store from "./store";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <React.StrictMode>
        <Provider store={store}>
            <App/>
        </Provider>
    </React.StrictMode>
);

about.jsx

jsx
import React, {Component} from 'react';
import {connect} from "react-redux";
import {changeCount} from "../store/actionCreator";

class About extends Component {
    render() {
        return (
            <div>
                <h2>About</h2>
                <span>About:{this.props.count}</span>
                
                <button onClick={() => {
                    this.props.changeCount(this.props.count + 10)
                }}>+10
                </button>
                
                <button onClick={() => {
                    this.props.changeCount(this.props.count - 10)
                }}>-10
                </button>
            </div>
        );
    }
}

const mapStateToProps = (state, ownProps) => ({
    count: state.count,
})

const mapDispatchToProps = (dispatch, ownProps) => ({
    changeCount: (count) => dispatch(changeCount(count))
})

export default connect(mapStateToProps, mapDispatchToProps)(About);

redux中处理异步操作

redux-thunk

redux-thunkredux进行了扩展,使得dispatch可以接收并执行一个函数,之前的dispatch只能接受对象,原理是利用了中间件

下面以一个实际项目场景作为例子,例如我们需要在Redux请求一个获取bannerList的接口,然后将数据存到Store

代码示例:

store/index.jsx

jsx
import {applyMiddleware, createStore} from "redux"
import reducer from "./reducer";
import {thunk} from "redux-thunk";

// 扩展中间件
const store = createStore(reducer, applyMiddleware(thunk))

export default store

store/actionCreator.js

jsx
export const changeBanner = (banner) => ({
    type: CHANGE_BANNER,
    banner
})

export const fetchBannerDataAction = (params) => {
    return (dispatch, getState) => {
        const banner = [
            {
                id: 1,
                title: 'banner title1'
            },
            {
                id: 2,
                title: 'banner title2'
            },
            {
                id: 3,
                title: 'banner title3'
            },
            {
                id: 4,
                title: 'banner title4'
            }
        ]
        console.log("参数", params)
        console.log("执行异步网络请求")
        // 延迟模拟网络请求
        setTimeout(() => {
            dispatch(changeBanner(banner))
        }, 2000)
    }
}

store/constants.js

jsx
export const CHANGE_BANNER = 'change_banner';

store/reducer.js

jsx
import * as Constans from "./constants";

const initialState = {
    count: 100,
    banner: []
}

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case Constans.CHANGE_COUNT:
            return {...state, count: action.count}
        case Constans.CHANGE_BANNER:
            console.log("更新CHANGE_BANNER")
            return {...state, banner: action.banner}
        default:
            return state
    }
}
export default reducer

代码中使用 about.jsx

jsx
import React, {Component} from 'react';
import {connect} from "react-redux";
import {changeCount, fetchBannerDataAction} from "../store/actionCreator";

class About extends Component {

    componentDidMount() {
        // 可以直接在props中使用
        this.props.fetchBannerData()
    }

    render() {

        const element = (
            (this.props.banner.length) && this.props.banner.map((item) => {
                return (
                    <li key={item.id}>title: {item.title}</li>
                )
            })
        )

        return (
            <div>
                <h2>About</h2>
                <span>About:{this.props.count}</span>

                <button onClick={() => {
                    this.props.changeCount(this.props.count + 10)
                }}>+10
                </button>

                <button onClick={() => {
                    this.props.changeCount(this.props.count - 10)
                }}>-10
                </button>

                <h2>banner</h2>
                <ul>
                    {element}
                </ul>
            </div>
        );
    }
}

const mapStateToProps = (state, ownProps) => ({
    count: state.count,
    banner: state.banner
})

const mapDispatchToProps = (dispatch, ownProps) => ({
    changeCount: (count) => dispatch(changeCount(count)),
    fetchBannerData: () => {
        // redux-thunk进行了扩展,使得dispatch可以接收并执行一个函数,之前的dispatch只能接受对象
        dispatch(fetchBannerDataAction({page: 1}))
    }
})

export default connect(mapStateToProps, mapDispatchToProps)(About);

调试工具

redux-devtools

react-developer-tools 第二个工具是react-developer-tools,类似vue-developer-tools,可以在谷歌商店下载

redux模块化拆分

在实际开发的过程中,如果我们所有需要管理的数据都放在同一个文件夹代码会比较混乱,正确的做法是根据业务或者功能进行模块化拆分,下面假设我们有两个功能模块bannercounter,接下来对其进行模块化拆分

拆分后的模块大概长这样:

banner模块: store/banner/actionCreator.js

js
import * as Constants from "./constants";

export const changeBanner = (banner) => ({
    type: Constants.CHANGE_BANNER,
    banner
})

export const fetchBannerDataAction = (params) => {
    return (dispatch) => {
        const banner = [
            {
                id: 1,
                title: 'banner title1'
            },
            {
                id: 2,
                title: 'banner title2'
            },
            {
                id: 3,
                title: 'banner title3'
            },
            {
                id: 4,
                title: 'banner title4'
            }
        ]
        console.log("参数", params)
        console.log("执行异步网络请求")

        setTimeout(() => {
            dispatch(changeBanner(banner))
        }, 2000)
    }
}

store/banner/constants.js

js
export const CHANGE_BANNER = 'change_banner';

store/banner/reducer.js

js
import * as Constans from "./constants";

const initialState = {
    banner: []
}

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case Constans.CHANGE_BANNER:
            console.log("更新CHANGE_BANNER")
            return {...state, banner: action.banner}
        default:
            return state
    }
}

export default reducer

store/banner/index.js

js
import reducer from './reducer';

export default reducer

export * from "./actionCreator"

counter模块: store/counter/actionCreator.js

js
import * as Constans from "./constants";

export const changeCount = (count) => ({
    type: Constans.CHANGE_COUNT,
    count
})

store/counter/constants.js

js
export const CHANGE_COUNT = 'change_count';

store/counter/reducer.js

js
import * as Constans from "./constants";

const initialState = {
    count: 100
}

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case Constans.CHANGE_COUNT:
            return {...state, count: action.count}
        default:
            return state
    }
}

export default reducer

store/counter/index.js

js
import reducer from './reducer';

export default reducer

export * from "./actionCreator"

关键点主要在store/index文件,这里使用了redux/combineReducers函数对多个reducer进行一个合并

js
import {applyMiddleware, createStore, compose, combineReducers} from "redux"
import {thunk} from "redux-thunk";
import Counter from "./counter";
import Banner from "./banner"

// 扩展redux-devtools中间件
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 扩展thunk中间件
let thunkEnhancers = applyMiddleware(thunk);
// 组合各个模块的reducer
const reducer = combineReducers({
    counter: Counter,
    banner: Banner
})

const store = createStore(reducer, composeEnhancers(thunkEnhancers))

export default store

combineReducers函数原理

combineReducers原理是对各个模块的reducer进行一个组合,它做的事情跟以下代码类似:

js
// combineReducers函数原理
function myCombineReducers(state = {}, action) {
    return {
        counter: counterReducer(state.counter, action),
        banner: bannerReducer(state.banner, action)
    }
}

上面的示例代码我们使用自己编写的combineReducers函数后运行代码也能得到预期结果 store/index

js
import {applyMiddleware, createStore, compose, combineReducers} from "redux"
import {thunk} from "redux-thunk";
import Counter from "./counter";
import Banner from "./banner"
import counterReducer from "./counter";
import bannerReducer from "./banner";

// 扩展redux-devtools中间件
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 扩展thunk中间件
let thunkEnhancers = applyMiddleware(thunk);

// 组合各个模块的reducer
// const reducer = combineReducers({
//     counter: Counter,
//     banner: Banner
// })

// combineReducers函数原理
function myCombineReducers(state = {}, action) {
    return {
        counter: counterReducer(state.counter, action),
        banner: bannerReducer(state.banner, action)
    }
}

const store = createStore(myCombineReducers, composeEnhancers(thunkEnhancers))

export default store

手写一个connect函数

connect接手两个参数:mapStateToPropsFnmapDispatchToPropsFn本质上connect的执行结果是一个高阶组件,参数接收了一个组件,返回值也是一个组件

js
import store from "../store";
import {PureComponent} from "react"

/**
 * 调用示例
 * const mapStateToProps = (state) => ({
 *     count: state.counter.count
 * })
 *
 * const mapDispatchToProps = (dispatch) => ({
 *     addNumber(count) {
 *         dispatch(addNumber(count))
 *     },
 *     // 两种写法都可以
 *     // addNumber: (count) => dispatch(addNumber(count))
 * })
 *
 * export default connect(mapStateToProps, mapDispatchToProps)(Home);
 */

/**
 * 函数返回结果是一个函数fn,fn接收参数是一个
 * react组件,目标是将mapStateToPropsFn、mapDispatchToPropsFn返回
 * 结果的对象作为参数传入到目标组件中
 * @param mapStateToPropsFn
 * @param mapDispatchToPropsFn
 */
export function myConnect(mapStateToPropsFn, mapDispatchToPropsFn) {

    let unsubscribe = null
    const dispatch = mapDispatchToPropsFn(store.dispatch)
    /**
     * 创建一个高阶函数,将props混入到WrapperCpn
     * @param WrapperCpn 待混入props的组件
     */
    const enhanceProps = (WrapperCpn) => {
        return class extends PureComponent {

            constructor(props) {
                super(props);

                this.state = {
                    // 将state声明在函数内部以方便在生命周期订阅store变化
                    storeState: mapStateToPropsFn(store.getState())
                }
            }

            componentDidMount() {
                unsubscribe = store.subscribe(() => {
                    this.setState({
                        storeState: mapStateToPropsFn(store.getState())
                    })
                })
            }

            componentWillUnmount() {
                unsubscribe()
            }

            render() {
                return <WrapperCpn {...this.props} {...this.state.storeState} {...dispatch} />
            }
        }
    }

    return enhanceProps
}

使用示例:

jsx
import React, {PureComponent} from 'react';
import {addNumber} from "../store/feature/counter";
import {myConnect} from "../utils/myConnect";

class Home extends PureComponent {
    render() {
        return (
            <div>
                <h2>
                    Home counter: {this.props.count}
                </h2>
                <div>
                    <button onClick={() => {
                        this.props.addNumber(1)
                    }}>+1
                    </button>

                    <button onClick={() => {
                        this.props.addNumber(5)
                    }}>+5
                    </button>
                </div>
            </div>
        );
    }
}

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

const mapDispatchToProps = (dispatch) => ({
    // 这种方式还不支持
    // addNumber(count) {
    //     dispatch(addNumber(count))
    // },
    addNumber: (count) => dispatch(addNumber(count))
})

export default myConnect(mapStateToProps, mapDispatchToProps)(Home);

使用Context优化connect函数

现在myConnect还有一些缺陷,就是依赖于项目中的store,当然也可以扩展myConnect第三个参数,从外部传入store进来,但这样会多写很多代码,我们可以参考react-redux的做法,借助ReactContext解决这个问题,这样我们的connect函数就不需要依赖项目中的store

重构后的代码结构:

src/hoc/StoreContext.js

js
import React from 'react'

export const StoreContext = React.createContext()

src/hoc/myConnect.js

js
import {PureComponent} from "react"
import {StoreContext} from "./StoreContext";

/**
 * 调用示例
 * const mapStateToProps = (state) => ({
 *     count: state.counter.count
 * })
 *
 * const mapDispatchToProps = (dispatch) => ({
 *     addNumber(count) {
 *         dispatch(addNumber(count))
 *     },
 *     // 两种写法都可以
 *     // addNumber: (count) => dispatch(addNumber(count))
 * })
 *
 * export default connect(mapStateToProps, mapDispatchToProps)(Home);
 */

/**
 * 函数返回结果是一个函数fn,fn接收参数是一个
 * react组件,目标是将mapStateToPropsFn、mapDispatchToPropsFn返回
 * 结果的对象作为参数传入到目标组件中
 * @param mapStateToPropsFn
 * @param mapDispatchToPropsFn
 */
export function myConnect(mapStateToPropsFn, mapDispatchToPropsFn) {

    /**
     * 创建一个高阶函数,将props混入到WrapperCpn
     * @param WrapperCpn 待混入props的组件
     */
    const enhanceProps = (WrapperCpn) => {
        class ConnectCpn extends PureComponent {

            constructor(props, context) {
                super(props);

                this.state = {
                    // 将state声明在函数内部以方便在生命周期订阅store变化
                    storeState: mapStateToPropsFn(context.getState())
                }
            }

            componentDidMount() {
                this.unsubscribe = this.context.subscribe(() => {
                    this.setState({
                        storeState: mapStateToPropsFn(this.context.getState())
                    })
                })
            }

            componentWillUnmount() {
                this.unsubscribe()
            }

            render() {
                const dispatchs = mapDispatchToPropsFn(this.context.dispatch)
                return <WrapperCpn {...this.props} {...this.state.storeState} {...dispatchs} />
            }

        }

        ConnectCpn.contextType = StoreContext

        return ConnectCpn
    }

    return enhanceProps
}

src/hoc/index.js

js
export { myConnect } from './myConnect';

export { StoreContext } from './StoreContext';

入口文件

js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import {Provider} from "react-redux";
import store from './store';
import {StoreContext} from "./hoc/index";

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Provider store={store}>
        <StoreContext.Provider value={store}>
            <App/>
        </StoreContext.Provider>
    </Provider>
);

使用示例:

jsx
import React, {PureComponent} from 'react';
import {addNumber} from "../store/feature/counter";
import {myConnect} from "../hoc/index";

class Home extends PureComponent {
    render() {
        return (
            <div>
                <h2>
                    Home counter: {this.props.count}
                </h2>
                <div>
                    <button onClick={() => {
                        this.props.addNumber(1)
                    }}>+1
                    </button>
                </div>
            </div>
        );
    }
}

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

const mapDispatchToProps = (dispatch) => ({
    addNumber: (count) => dispatch(addNumber(count))
})

export default myConnect(mapStateToProps, mapDispatchToProps)(Home);

dispatch打印日志

代码如下,我们只需要在抛出store之前,编写一个函数对store.dispatch进行修改即可 store/index

js
/**
 * 传入store,当调用store.dispatch的时候打印日志
 * @param store
 */
function patchLog(store){
    const next = store.dispatch

    function dispatchAndLog(action){
        console.log("执行dispatch")
        next(action)
        console.log("dispatch执行完毕")
    }

    store.dispatch = dispatchAndLog

    return store
}

export default patchLog(store)

实现redux-thunk中间件

store/index.js

js
/**
 * 实现thunk中间件支持执行函数功能
 * @param store
 */
function patchThunk(store){
    const next = store.dispatch

    function dispatchAndThunk(action){
        if(typeof(action) === "function"){
            // 传入的是函数
            action(next,store.getState)
        }else{
            // 传入的是对象
            next(action)
        }
    }

    store.dispatch = dispatchAndThunk;
}


const store = createStore(reducer)

patchThunk(store)

export default store

合并中间件

js
/**
 * 实现thunk中间件支持执行函数功能
 * @param store
 */
function patchThunk(store){
    const next = store.dispatch

    function dispatchAndThunk(action){
        if(typeof(action) === "function"){
            // 传入的是函数
            action(next,store.getState)
        }else{
            // 传入的是对象
            next(action)
        }
    }

    store.dispatch = dispatchAndThunk;
}

/**
 * 传入store,当调用store.dispatch的时候打印日志
 * @param store
 */
function patchLog(store){
    const next = store.dispatch

    function dispatchAndLog(action){
        console.log("执行dispatch")
        next(action)
        console.log("dispatch执行完毕")
    }

    store.dispatch = dispatchAndLog
}

/**
 * 传入多个中间件进行合并
 * @param store
 * @param middlewares
 */
function applyMiddleware(store,...middlewares){
    middlewares.forEach(fn => {
        fn(store)
    })
}

const store = createStore(reducer)

applyMiddleware(store,patchLog,patchThunk)

export default store

react中state如何管理

上次更新于: