react系列-redux

官方网站:https://redux.js.org/

中文文档:https://cn.redux.js.org/

基础

目的

状态管理器

JavaScript 需要管理的状态越来越多,越来越复杂;

这些状态包括服务器返回的数据、缓存数据、用户操作生产的数据等等,也包括一些 UI 的状态,比如某些元素是否被选中,是否显示加载动效,当前分页等;

当应用程序复杂时,state 在什么时候,因为什么原因而发生了变化,发生了怎样的变化,会变得非常难以控制和追踪。

定义

Redux 是 JavaScript 的状态容器,提供了可预测的状态管理。

跟框架无关,可以结合 vue 等其他框架使用。

核心理念

redux = reducer + flux

store

保存 state 数据

action

Redux 要求我们通过 action 来更新数据:

  • 所有数据的变化,必须通过派发(dispatch)action 来更新;
  • action 是一个普通的 JavaScript 对象,用来描述这次更新的 type 和 content;
reducer

reducer 将 state 和 action 联系起来

reducer 是一个纯函数

reducer 做的事情就是将传入的 state 和 action 结合起来生成一个新的 state

原则

单一数据源、单向数据流

store 是唯一的。

整个应用程序的state被存储再一颗 object tree 中,并且这个 object tree只存储在一个 store 中;

Redux 并没有强制让我们不能创建多个 Store,但是那样做不利于数据的维护;

单一的数据源可以让整个应用程序的 state 变得方便维护、追踪、修改;

State是只读的

唯一修改 State 的方法是触发 action,不要视图在其他地方通过任何的方式来修改 State;

这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心 race condition(竞态)的问题。

只有 store 更改 state 数据内容,reducer 可以接受 state,但不能改变 state。

使用纯函数来执行修改

通过 reducer 将旧 state 和 actions 联系在一起,并且返回一个新的 state;

随着应用程序的复杂度增加,我们可以将 reducer 拆分成多个小的 reducers,分别操作不同 state tree的一部分;

所有的 reducer 都应该是纯函数,不能产生任何副作用。

redux 数据交互流程

redux使用流程

redux数据流动方向:单向数据流

ReduxDataFlowDiagram

异步请求

网络请求到的数据属于状态管理的一部分,所以请求数据应该由 redux 进行管理。

然而,redux 本身没有异步请求的概念,需要借助中间件进行实现。

redux 中间件的作用是对 store 的 dispatch 方法进行升级,如果传入的是函数,会将函数执行后再进行其他操作。

redux_async

redux-thunk

官方推荐,用于处理store中的异步事件。

一般使用 actionCreateor 返回的是对象,使用 redux-thunk 后可以返回异步函数,action 为函数,store.dispatch(action) 执行的是函数并获取返回结果。

redux-saga

redux-saga 使用过程中,在 store.dispatch(action)时,store 和 saga 能够同时接收到 action 参数

redux-saga 和 redux-thunk 的作用相同,用法不同。

redux-thunk 提供的接口少,适用于中小型项目。

redux-saga 有丰富的接口,适用于逻辑复杂的大型项目。

中间件

api 文档:https://redux.js.org/api/applymiddleware

redux 使用中间件

store/index.js

import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import reducer from './reducer'

// 应用一些中间件
const storeEnhancer = applyMiddleware(thunkMiddleware)

const store = createStore(reducer, storeEnhancer)

export default store

actionCreators.js

// redux-thunk 中定义的函数
export const getHomeMultidataAction = dispatch => {
    axios({
        url: 'http://123.207.32.32:8000/home/multidata'
    }).then(res => {
        const { data: { banner: { list: banners }, recommend: { list: recommends } } } = res.data
        console.log(banners)
        dispatch(changeBannersAction(banners))
        dispatch(changeRecommendsAction(recommends))
    })
}
实现简单的中间件(中间件原理)
const redux = require('redux')

const actionTypes = {
    ADD_NUMBER: 'ADD_NUMBER',
    SUB_NUMBER: 'SUB_NUMBER'
}

const actionCreators = {
    addAction(num) {
        return {
            type: actionTypes.ADD_NUMBER,
            num
        }
    },
    subAction(num) {
        return {
            type: actionTypes.SUB_NUMBER,
            num
        }
    },
    thunkAction(dispatch, getState) {
        dispatch(actionCreators.subAction(1))
        console.log('state: ', getState())
    }
}

const initialState = {
    counter: 0
}

// reducer 
// 默认情况下 初始化state是没有值的,所以此时要赋值初始值
function reducer(state = initialState, action) {
    switch (action.type) {
        case actionTypes.ADD_NUMBER:
            return {...state, counter: state.counter + action.num}
        case actionTypes.SUB_NUMBER:
            return {...state, counter: state.counter - action.num}
        default:
            return state
    }
}

// store (创建的时候需要传入 reducer)
const store = redux.createStore(reducer)

// 调用 ====================================================
// store.dispatch(actionCreators.addAction(10))
// console.log(store.getState())


// 根据需求封装中间件
// 需求:在 dispatch 之前打印一下数据,在 dispatch 之后打印一下数据

// 方式一 创建一个函数进行包裹
// function dispatchAndLogging(action) {
//     console.log('dispatch before--dispatching action: ', action)
//     store.dispatch(action)
//     console.log('dispatch after--new state: ', store.getState())
// }

// dispatchAndLogging(actionCreators.addAction(10))

// 方式二 hack技巧  monkeyingpatch 将原有的api重新进行定义 使用时依旧按照原来的使用方式进行
function patchLogging(store) {
    const next = store.dispatch 
    function dispatchAndLogging(action) {
        console.log('dispatch before--dispatching action: ', action)
        next(action)
        console.log('dispatch after--new state: ', store.getState())
    }
    store.dispatch = dispatchAndLogging
    // return dispatchAndLogging // 使用 applyMiddlewares
}

// 同理 封装 thunk 方法 处理 actionCreator是函数的情况
function patchThunk(store) {
    const next = store.dispatch
    function dispatchAndThunk(action) {
        if (typeof action === 'function') {
            action(next, store.getState)
        } else {
            next(action)
        }
    }
    store.dispatch = dispatchAndThunk
    // return dispatchAndThunk // 使用 applyMiddlewares
}

// 封装 applyMiddlewares 需要中间件函数返回纯函数的方式进行
// function applyMiddlewares(...middlewares) {
//     middlewares.forEach(middleware => {
//         store.dispatch = middleware(store)
//     })
// }
// applyMiddlewares(patchLogging, patchThunk)

patchLogging(store) // 装载 store.dispatch 
patchThunk(store)
store.dispatch(actionCreators.addAction(10)) // 依旧按照原来的方式进行调用
store.dispatch(actionCreators.thunkAction)

实现中间件的一种方式,还有其他方式,可以参考其他 redux 中间件源码。

实战

node + redux

const redux = require('redux')

// 常量
// const actionTypes = {
//     INCREMENT: 'INCREMENT',
//     DECREMENT: 'DECREMENT',
//     ADD_NUMBER: 'ADD_NUMBER',
//     SUB_NUMBER: 'SUB_NUMBER'
// }
const actionTypes = {
    INCREMENT: Symbol('INCREMENT'),
    DECREMENT: Symbol('DECREMENT'),
    ADD_NUMBER: Symbol('ADD_NUMBER'),
    SUB_NUMBER: Symbol('SUB_NUMBER')
}

// 初始值
const initialState = {
    counter: 0
}

// reducer 
// 默认情况下 初始化state是没有值的,所以此时要赋值初始值
function reducer(state = initialState, action) {
    switch (action.type) {
        case actionTypes.INCREMENT:
            return {...state, counter: state.counter + 1}
        case actionTypes.DECREMENT:
            return {...state, counter: state.counter - 1}
        case actionTypes.ADD_NUMBER:
            return {...state, counter: state.counter + action.num}
        case actionTypes.SUB_NUMBER:
            return {...state, counter: state.counter - action.num}
        default:
            return state
    }
}

// store (创建的时候需要传入 reducer)
const store = redux.createStore(reducer)

// 订阅 store 的修改 要在派发action之前做
store.subscribe(() => {
    console.log('state 发生了修改', store.getState().counter)
})

// actions 
const action1 = {type: actionTypes.INCREMENT}
const action2 = {type: actionTypes.DECREMENT}

const action3 = {type: actionTypes.ADD_NUMBER, num: 5}
const action4 = {type: actionTypes.SUB_NUMBER, num: 12}
// 函数方式动态设置传入参数
const action5 = num => {
    return {type: actionTypes.ADD_NUMBER, num}
}

// 派发 action 然后执行 reducer纯函数进行处理数据
store.dispatch(action1)
store.dispatch(action2)
store.dispatch(action3)
store.dispatch(action4)
store.dispatch(action5(90))

使用 symbol类型定义 action type 不利于 redux devtool 跟踪查看数据变化

react + redux

使用 redux 的组件示例

import React, { PureComponent } from 'react'
import store from '../../store'
import {
    increment,
    addNumber
} from '../../store/actionCreators'

export default class Home extends PureComponent {

    constructor(props) {
        super(props)

        this.state = {
            counter: store.getState().counter
        }
    }

    componentDidMount() {
        // 订阅
        this.unsubscribe = store.subscribe(() => {
            this.setState({
                counter: store.getState().counter
            })
        })
    }

    componentWillUnmount() {
        // 取消订阅
        this.unsubscribe()
    }

    render() {
        return (
            <div>
                <h4>Home - 当前计数: {this.state.counter}</h4>
                <div>
                    <button onClick={e => this.increment()}>+1</button>
                    <button onClick={e => this.addNumber(5)}>+5</button>
                </div>
            </div>
        )
    }

    increment() {
        store.dispatch(increment())
    }

    addNumber(num) {
        store.dispatch(addNumber(num))
    }
}
抽取 redux 与 组件链接的公共代码
使用 store 进行数据交互

使用高阶组件封装 工具函数

import React, { PureComponent } from "react"
import store from "../store"

/**
 * 建立 react 与 redux 的连接
 * @param {function} mapStateToProps 组件中需要使用的 store 中的数据映射到 props 中,返回值为对象
 * @param {function} mapDispachToProp 组件中需要派发的 action 映射到 props 中,返回值为对象
 */
export function connect(mapStateToProps, mapDispachToProp) {
    // 高阶组件
    return function enhanceHOC(WrappedComponend) {
        return class extends PureComponent {
            constructor(props) {
                super(props)

                this.state = {
                    // 返回组件中需要的 state
                    storeState: mapStateToProps(store.getState())
                }
            }

            componentDidMount() {
                // 建立监听
                this.unsubscribe = store.subscribe(() => {
                    this.setState({
                        storeState: mapStateToProps(store.getState())
                    })
                })
            }

            componentWillUnmount() {
                // 取消监听
                this.unsubscribe()
            }

            render() {
                // props 穿透
                return <WrappedComponend 
                        {...this.props} 
                        {...mapStateToProps(store.getState())} 
                        {...mapDispachToProp(store.dispatch)} />
            }
        }
    }
}

组件使用

import React from 'react'
import connect from '../../utils/connect'
import {
    increment,
    addNumber
} from '../../store/actionCreators'

function Home(props) {
    return (
        <div>
            <h4>Home - 当前计数: {props.counter}</h4>
            <div>
                <button onClick={e => props.increment()}>+1</button>
                <button onClick={e => props.addNumber(5)}>+5</button>
            </div>
        </div>
    )
}

const mapStateToProps = state => {
    return {
        counter: state.counter
    }
}

const mapDipatchToProps = dispatch => {
    return {
        increment() {
            dispatch(increment())
        },
        addNumber(num) {
            dispatch(addNumber(num))
        }
    }
}

export default connect(mapStateToProps, mapDipatchToProps)(Home)
进一步优化,抽离 store 数据

减少业务代码耦合,使用 context 进行抽离业务数据。

context.js

import React from 'react'

const StoreContext = React.createContext()

export {
    StoreContext
}

connect.js

// 优化 connect 工具函数 将 store 利用 context 抽离出去
// 减少业务代码耦合

import React, { PureComponent } from "react"
import { StoreContext } from "./context"

export default function connect(mapStateToProps, mapDispachToProp) {
     // 高阶组件
     return function enhanceHOC(WrappedComponend) {
        class EnhanceComponent extends PureComponent {
            constructor(props, context) {
                super(props, context)

                this.state = {
                    // 返回组件中需要的 state
                    storeState: mapStateToProps(context.getState())
                }
            }

            componentDidMount() {
                // 建立监听
                this.unsubscribe = this.context.subscribe(() => {
                    this.setState({
                        storeState: mapStateToProps(this.context.getState())
                    })
                })
            }

            componentWillUnmount() {
                // 取消监听
                this.unsubscribe()
            }

            render() {
                // props 穿透
                return <WrappedComponend 
                        {...this.props} 
                        {...mapStateToProps(this.context.getState())} 
                        {...mapDispachToProp(this.context.dispatch)} />
            }
        }

        EnhanceComponent.contextType = StoreContext

        return EnhanceComponent
    }
}

父组件使用,将store与context建立连接,提供数据

import React from 'react'
import Home from './Home'
import About from './About'
import { StoreContext } from '../../utils/context'
import store from '../../store'

export default function UseConnect() {
    return (
        <StoreContext.Provider value={ store }>
            <Home></Home>
            <About></About>
        </StoreContext.Provider>
    )
}

子组件中和 store 方法一致

相关第三方库

react-redux :官方库,灵活绑定 react 和 redux 。

拆分 reducer

import { combineReducers } from 'redux'
import * as actionTypes from './actionTypes'

// 初始值
// const initialState = {
//     counter: 0,
//     banners: [],
//     recommends: [],
// }

// function reducer(state = initialState, action) {
//     switch (action.type) {
//         case actionTypes.INCREMENT:
//             return {...state, counter: state.counter + 1}
//         case actionTypes.DECREMENT:
//             return {...state, counter: state.counter - 1}
//         case actionTypes.ADD_NUMBER:
//             return {...state, counter: state.counter + action.num}
//         case actionTypes.SUB_NUMBER:
//             return {...state, counter: state.counter - action.num}
//         case actionTypes.CHANGE_BANNERS:
//             return {...state, banners: action.banners}
//         case actionTypes.CHANGE_RECOMMENDS:
//             return {...state, recommends: action.recommends}
//         default:
//             return state
//     }
// }

// reducer 拆分 state 调用的地方需要一起修改
const defaultCounterState = {
    counter: 0
}

function counterReducer(state = defaultCounterState, action) {
    switch (action.type) {
        case actionTypes.INCREMENT:
            return {...state, counter: state.counter + 1}
        case actionTypes.DECREMENT:
            return {...state, counter: state.counter - 1}
        case actionTypes.ADD_NUMBER:
            return {...state, counter: state.counter + action.num}
        case actionTypes.SUB_NUMBER:
            return {...state, counter: state.counter - action.num}
        default:
            return state
    }
}

const defaultHomeState = {
    banners: [],
    recommends: []
}
function homeReducer(state = defaultHomeState, action) {
    switch (action.type) {
        case actionTypes.CHANGE_BANNERS:
            return {...state, banners: action.banners}
        case actionTypes.CHANGE_RECOMMENDS:
            return {...state, recommends: action.recommends}
        default:
            return state
    }
}

// function reducer(state = {}, action) {
//     return {
//         counterInfo: counterReducer(state.counterInfo, action),
//         homeInfo: homeReducer(state.homeInfo, action)
//     }
// }
// 最终的结果是 reducer 返回和 state = { counterInfo: {}, homeInfo: {} } 一样的对象格式

// 使用 redux combineReducers(reducers) api 进一步优化
const reducer = combineReducers({ 
    counterInfo: counterReducer, 
    homeInfo: homeReducer 
})

export default reducer

combineReducers 函数 将我们传入的 reducers 合并到一个对象中,最终返回一个 combination 的函数,在执行 combination 函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的 state 还是新的 state。新的 state 会触发订阅者发生对应的刷新,而旧的 state 可以有效的阻止订阅者发生刷新。

结合 ImmutableJs

ImmutableJs :使用不可变的数据。

redux-immutable:使用 combineReducer 函数结合redux 和 immutable.js 的功能。

fromJS 会进行深层比较,而 map 会进行浅层的比较,根据业务需求来确定使用哪一个 api ,fromJS 的性能会比 map 的性能更低。

更多阅读: https://redux.js.org/faq/immutable-data

性能优化

扩展

生态:https://redux.js.org/introduction/ecosystem

工具

redux devtool :不同的场景需要手动设置使用方式,https://github.com/reduxjs/redux-devtools/tree/master/extension#usage

基础使用方式,需要在创建 store 时声明使用

const store = createStore(
  reducer, /* preloadedState, */
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);