(5k字)[ "干中之干系列3" ] —— 带刀护卫Redux

381 阅读15分钟

Redux对于React来讲,就相当于只听命于皇上的锦衣卫一样,朝中不论大事小事都在锦衣卫的监管之下,皇上想要了解一下大臣最近的具体情况,只需要问锦衣卫即可,比如某个大臣未来可能要谋反,在其计划实施之前就会被锦衣卫发现,之后汇报给皇上,这样在谋反行动还未开始时就被解决掉了。redux就扮演了这样一个角色,把所有模块中需要的状态和数据,通通收集起来统一管理,并且状态更新是可预测的、可追踪的,总而言之一切都在掌控之中。

PS:本文不讨论 Redux 的缺点

一、redux 的实现过程

首先我们先来认识三个单词

store 容器
reducer 减速器
dispatch 派发

Store

Store是一个JavaScript对象,用于管理应用程序的状态。Store将应用程序的状态存储在一个单一的对象中,并提供了一些方法来访问和更新该状态。Store 是 Redux 架构中的核心概念之一,它将Reducer、状态和动作对象联系在一起
Redux中的Store具有以下特点:

  • 单一状态树: 所有应用程序状态都存储在一个单一的JavaScript对象中,这个对象被称为状态树。
  • 只读状态: 不能直接修改状态,必须通过dispatch一个action来描述状态变化。
  • 纯函数更新状态: 使用纯函数reducer处理动作,更新应用程序状态。

在Redux中,可以使用createStore函数来创建一个Store。createStore函数接收一个Reducer函数和一个可选的初始状态对象作为参数,并返回一个Store对象。可以使用Store对象提供的方法来访问和更新应用程序的状态。

import { createStore } from 'redux';
import rootReducer from './reducers';

const initialState = {
  todos: [],
  filter: 'SHOW_ALL'
};

const store = createStore(rootReducer, initialState);

export default store;

在上面的代码中,我们使用createStore函数创建了一个Redux Store,并将其导出。我们将应用程序的初始状态传递给createStore函数,以及一个由多个Reducer函数合并而成的rootReducer函数。这个Store对象可以用于访问和更新应用程序的状态。

Redux的Store对象提供了以下方法:

  • getState(): 获取当前的状态对象。
  • dispatch(action): 分发一个动作对象,触发状态更新。
  • subscribe(listener): 添加一个监听器函数,每当状态更新时被调用。
  • replaceReducer(nextReducer): 用于动态替换Store中的Reducer函数。

使用Store对象的这些方法,我们可以访问和更新Redux应用程序的状态,并响应状态的变化。

Reducer

Reducer也是Redux架构中的核心概念之一,它负责处理所有的状态变化,而且必须是纯函数。每当动作被触发时,Redux会自动调用所有注册的Reducer函数,并将当前的状态和动作对象作为参数传递给它们。Reducer函数根据动作类型来更新状态,并返回新的状态对象。

let initial = {
  count:10
};

const counterReducer = function (state = initial, action) {
state = {...state};
  switch (action.type) {
    case 'INCREMENT':
      state.count++;
      break;
    case 'DECREMENT':
      state.count--;
      break
    default:
  };
  return state;
}

export default counterReducer;

在上面的代码中,我们定义了一个counterReducer函数,它接收一个初始状态对象和一个动作对象作为参数。根据动作类型来更新状态,并返回新的状态对象。如果动作类型不匹配,Reducer就会返回当前的状态对象。

在Redux中,可以将多个Reducer函数合并成一个大的Reducer函数,用于管理整个应用程序的状态。合并Reducer函数的方法是使用Redux提供的combineReducers函数,该函数可以将多个Reducer函数合并成一个Reducer函数。

import { combineReducers } from 'redux';
import counterReducer from './counterReducer';
import todosReducer from './todosReducer';

const rootReducer = combineReducers({
  counter: counterReducer,
  todos: todosReducer
});

export default rootReducer;

Dispatch

在Redux中,dispatch是一个用于触发状态变化的函数,它接收一个动作对象作为参数,并将该动作对象传递给注册的Reducer函数。Reducer函数会根据动作对象的类型来更新状态,并返回一个新的状态对象。这个新的状态对象会替换原有的状态对象,从而触发应用程序的重新渲染。

const ADD_TODO = 'ADD_TODO';

function increment() {
  return {
    type: ADD_TODO
  };
}

store.dispatch(increment());

dispatch函数的参数是一个动作对象(Action Object)。动作对象是一个简单的JavaScript对象,它至少包含一个type属性,用于指定动作的类型,以便在Reducer函数中进行识别和处理。

动作对象可以包含其他属性,以便传递一些附加的数据,这些数据将被用于更新应用程序的状态。例如,以下是一个包含类型和数据属性的动作对象:

const ADD_TODO = 'ADD_TODO';

function addTodo(id, text) {
  return {
    type: ADD_TODO,
    payload: {
      id,
      text
    }
  };
}

//需要分发时
store.dispatch(addTodo(1, 'Learn Redux'));

dispatch函数使得应用程序的状态变得可预测和可控,而且可以方便地进行调试和测试。

总结

Redux是一个JavaScript状态管理库,用于在应用程序中管理复杂的状态。Redux通过使用一个中央存储对象Store作为应用程序的状态容器,将所有的状态存储在一个单一的地方,使得状态的变化和管理变得更加容易和可预测。

Redux中的状态变化是通过调用纯函数的Reducer来处理的。Reducer是一个接受当前状态和动作对象作为参数的函数,并返回一个新的状态对象。Redux的Reducer被称为减速器(Reducer)是因为它们的主要作用是减慢状态变化的速度,确保状态变化的可预测性和一致性。

为了改变应用程序的状态,我们需要派发Dispatch一个动作(Action)对象到Redux的Store中。Dispatch是一个用于向Store发送动作的方法,它将动作对象作为参数,并将其发送到Redux的Store中。一旦Store接收到一个动作,它会调用相应的Reducer来更新状态,并通知所有已注册的状态监听器函数。

二、redux工程化

目录结构

20230402192658.png

actions文件夹中

//以 aAction.js 为例
const aAction = {
  SUP() {
    return {
      type: 'TYPE_SUP'
    };
  },
  OPP() {
    return {
      type: 'TYPE_OPP'
    }
  }
}
export default aAction;
// index.js
import aAction from "./aAction";
import bAction from "./bAction";

const action = {
  aA: aAction,
  bA: bAction
}

export default action;

reducers文件夹中

//以 aReducer.js 为例
const initial = {
    supNum: 10,
    oppNum: 5,
    num: 0
};
export default function voteReducer(state = initial, action) {
    state = { ...state };
    switch (action.type) {
        case 'TYPE_SUP':
            state.supNum++;
            break;
        case 'TYPE_OPP':
            state.oppNum++;
            break;
        default:
    }
    return state;
};
// index.js
import { combineReducers } from 'redux';
import aReducer from './aReducer';
import bReducer from './bReducer';

const reducer = combineReducers({
    aR: aReducer,
    bR: bReducer
});
export default reducer;

store文件夹中,把最高级 reducer 放到容器中

// index.js
import { createStore } from 'redux';
import reducer from './reducers';

const store = createStore(reducer);
export default store;

创建上下文对象

//在ThemeContext.js中
import React from "react";
const ThemeContext = React.createContext();
export default ThemeContext;

在入口文件中,把 store 挂载到上下文容器中

import ReactDOM from 'react-dom/client';
import App from './views/App';
import store from './store'
import ThemeContext from './ThemeContext';

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

使用

在 views 文件夹中

//App.jsx中
import React, { useContext, useEffect, useState } from "react";
import A from './A';
import B from './B';
import ThemeContext from "../ThemeContext";

const App = function() {
    const { store } = useContext(ThemeContext);
    let { supNum, oppNum } = store.getState().aR;
    let [num, setNum] = useState(0);

   //手动把让组件更新的方法,放到事件池中
    useEffect(() => {
      const unsubscribe = store.subscribe(() => {
        setNum(Date.now())
      })
      return () => {
        unsubscribe()
      }
    }, [num])

    return <div>
         <P>{supNum+oppNum}</P>
        <A />
        <B />
    </div>;
};

export default App;

在A类组件中

import React from "react";
import ThemeContext from "../ThemeContext";

class A extends React.Component {
    static contextType = ThemeContext;

    render() {
        const { store } = this.context;
        let { supNum, oppNum } = store.getState().aR;

        return <div>
            <p>{supNum}</p>
            <p>{oppNum}</p>
        </div>;
    }
}

export default A;

在B函数组件中

import action from '../store/actions'

const B = function () {
    const { store } = useContext(ThemeContext);

    return <div>
        <button 
            onClick={() => {
                store.dispatch(action.aA.SUP())
            }}>
            支持
        </button>

        <button 
            onClick={() => {
                store.dispatch(action.aA.OPP())
            }}>
            反对
        </button>
    </div >;
};
export default B;

这样一个简单的redux工程化Demo就完成了

三、react-redux

React-Redux是Redux的官方React绑定库。它提供了一组React组件和API,使React应用程序可以使用Redux来管理其状态。它包含了一个提供了connect()函数的Provider组件和一个connect()函数,让组件可以连接到Redux存储并访问状态和操作。在React-Redux中,组件订阅Redux存储中特定状态的更改,以便它们可以在状态更改时自动重新渲染。这使得React组件可以使用Redux来管理其状态,而无需将状态作为本地组件状态存储在组件中。
简单点说就是使用起来更方便了

在之前的工程目录中,不需要我们自己去创建上下文对象了

在入口文件中

import ReactDOM from 'react-dom/client';
import App from './views/App';
/* REDUX */
import { Provider } from 'react-redux'
import store from './store'

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

在 Views 文件夹中

//App.jsx
import A from './A';
import B from './B';
import { connect } from 'react-redux'

const App = function (props) {
    let { supNum, oppNum } = props
    return <div>
         <P>{supNum+oppNum}</P>
        <A />
        <B />
    </div>;
};
export default connect(state => state.aR)(App);
//A.jsx
import React from "react";
import { connect } from 'react-redux'

class A extends React.Component {

    render() {
        let { supNum, oppNum } = this.props;

        return <div >
            <p>{supNum}人</p>
            <p>{oppNum}人</p>
        </div>;
    }
}

export default connect(state => state.aR)(A);
//B.jsx
import React, { useContext } from "react";
import { connect } from 'react-redux'
import action from '../store/actions'

const B = function (props) {
    let { SUP, OPP } = props

    return <div>
        <button 
            onClick={SUP}>
            支持
        </button>
        <button 
            onClick={OPP}>
            反对
        </button>
    </div >;
};
export default connect(
    null, 
    action.aA
)(B);

重点

公式:connect (mapStateToProps, mapDispatchToProps) (渲染的组件)

connect函数有两个参数,第一个参数是一个函数,称为mapStateToProps,它接受整个store作为参数,返回一个对象,这个对象将作为props传递给被包装的组件。这个函数可以选择性地返回一个子集的store,只要该子集发生变化,被包装的组件就会重新渲染。

第二个参数是一个函数,称为mapDispatchToProps,它接受一个dispatch函数作为参数,返回一个对象,这个对象包含了一些函数,这些函数也将作为props传递给被包装的组件。这些函数用于派发action,以便更改store中的状态。

connect函数返回一个新函数,这个函数接受一个组件作为参数,返回一个已经连接到store的组件。这个新的组件会将mapStateToProps和mapDispatchToProps函数的返回值合并成一个props对象,传递给被包装的组件。

使用connect函数,可以将store中的状态和派发action的函数都映射到React组件的props中,从而实现React组件与store的连接。这使得React组件可以直接从store中获取数据,而不需要手动订阅(subscribe)store的变化。

四、redux 中间件

Redux中间件是一个可插入的代码层,可以在Redux应用程序中的“action”和“reducer”之间添加额外的逻辑。它们允许您将应用程序的行为分解为独立的功能块,每个块都可以独立地编写、测试和重用。

使用中间件之前需要先通过 applyMiddleware 进行注册

在store文件下

//index.js
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducers';
import reduxLogger from 'redux-logger';
import reduxThunk from 'redux-thunk';
import reduxPromise from 'redux-promise';

const store = createStore(
    reducer,
    applyMiddleware(reduxLogger, reduxThunk, reduxPromise)
);
export default store;

redux-logger

使用 Logger 中间件时,每当 store 中的状态发生变化时,中间件都会将新的状态和相关的操作记录到控制台中。这可以让我们更好地调试和分析Redux应用程序。

//举个例子
import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';
import rootReducer from './reducers';

const store = createStore(
  rootReducer,
  applyMiddleware(logger)
);

store.dispatch({ type: 'INCREMENT' });
// Output: "dispatch { type: 'INCREMENT' }"
// Output: "next state { counter: 1 }"

store.dispatch({ type: 'DECREMENT' });
// Output: "dispatch { type: 'DECREMENT' }"
// Output: "next state { counter: 0 }"

redux-thunk 和 redux-promise

thunk中间件可以在“action creator”中返回一个函数,而不是一个纯对象。这个函数可以在后台执行任何异步操作,并且在完成后触发一个真正的“action”。这个函数被称为“thunk函数”,它接收两个参数:dispatch和getState。dispatch是store的dispatch函数,getState是一个函数,它返回当前的state。
Promise中间件可以在“action creator”中返回一个Promise对象,当一个“action”返回一个Promise对象时,Promise中间件会等待Promise完成后再将它的结果作为“action”的payload传递给reducer。如果Promise被拒绝,中间件将触发一个包含错误信息的新“action”。

在actions文件中

//aAction.js
// 延迟函数:返回promise实例,在指定的时间后,才会让实例为成功
const delay = (interval = 1000) => {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve();
        }, interval);
    });
};

const aAction = {
    // redux-thunk中间件语法
    SUP() {
        return async (dispatch) => {
            await delay();
            dispatch({
                type: TYPE_SUP
            });
        };
    },
    // redux-promise中间件语法
    async OPP() {
        await delay(2000);
        return {
            type: TYPE_OPP
        };
    }
};
export default aAction;

五、Redux Toolkit

Redux Toolkit是一个官方推荐的Redux工具包,它提供了一组简化Redux开发的工具和约定。使用Redux Toolkit,我们可以更快地编写Redux代码,并减少重复性的模板代码。

Redux Toolkit主要提供了三个功能:

configureStore

configureStore函数用于创建一个store。它接受一个对象作为参数,该对象包含以下属性:

  1. reducer:Redux store中的reducer。可以是一个单独的reducer函数,也可以是由多个reducer函数组成的对象。可以使用combineReducers函数将多个reducer函数组合成一个对象。
  2. middleware:Redux store中使用的中间件。可以是一个单独的中间件函数,也可以是一个中间件函数数组。可以使用applyMiddleware函数将多个中间件函数组合成一个中间件函数。
  3. devTools:一个布尔值或一个devTools配置对象,用于启用或禁用Redux DevTools扩展。如果传递了一个对象,则可以指定扩展的名称、配置和其他选项。
  4. preloadedState:Redux store的预加载状态。可以是任何类型的值,例如对象、数组或原始值。
  5. enhancers:Redux store的增强器。可以是单独的增强器函数,也可以是增强器函数数组。可以使用compose函数将多个增强器函数组合成一个增强器函数。
import { configureStore, combineReducers, applyMiddleware } from '@reduxjs/toolkit';
import thunkMiddleware from 'redux-thunk';
import loggerMiddleware from 'redux-logger';
import { todosReducer } from './todos';
import { userReducer } from './user';

const rootReducer = combineReducers({
  todos: todosReducer,
  user: userReducer,
});

const middleware = [thunkMiddleware, loggerMiddleware];

const store = configureStore({
  reducer: rootReducer,
  middleware: applyMiddleware(...middleware),
  devTools: process.env.NODE_ENV !== 'production',
});

export default store;

在这个例子中,首先使用combineReducers函数将两个reducer函数组合成一个根reducer函数。然后,我们使用applyMiddleware函数将两个中间件函数组合成一个中间件函数数组。最后使用configureStore函数创建了一个Redux store,并将根reducer函数、中间件函数数组和开发工具配置作为参数传递。

createSlice

createSlice函数用于创建一个包含reducer和action creators的slice。它接受一个对象作为参数,该对象包含以下属性:

  1. name(必填):slice的名称。它将用于创建action types和action creators的前缀。
  2. initialState(必填):slice的初始状态。可以是任何类型的值,例如对象、数组或原始值。
  3. reducers:一个对象,包含用于更新slice状态的reducer函数。每个reducer函数都将接受slice的当前状态和一个action对象作为参数,并返回一个新的slice状态。注意,createSlice会自动创建action creators,这些action creators的名称与reducer函数的名称相同。
  4. extraReducers:一个对象,用于处理其他slice的action。与reducers不同,extraReducers将使用字符串类型的action type作为键,而不是使用action creator函数的名称。该对象的值是一个reducer函数,它接受slice的当前状态和一个action对象作为参数,并返回一个新的slice状态。
import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
  name: 'counter',
  initialState: 0,
  reducers: {
    increment: (state) => state + 1,
    decrement: (state) => state - 1,
    reset: () => 0,
  },
});

export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;

在这个例子中,我们使用createSlice函数创建了一个名为counter的slice。slice的初始状态为0,reducers对象包含三个reducer函数:increment、decrement和reset。createSlice会自动创建与reducer函数名称相同的action creators。我们可以将这些action creators导出并在其他文件中使用。最后,我们将slice的reducer导出,以便在Redux store中使用。

createAsyncThunk

createAsyncThunk函数接受两个参数:typePrefix和一个async callback函数。

  1. typePrefix是一个字符串,用于创建异步操作的相关action types。createAsyncThunk函数会自动生成三个action types,包括 typePrefix/pending、typePrefix/fulfilled、typePrefix/rejected。

  2. async callback函数是一个异步的回调函数,它的返回值可以在fulfilled action中作为payload传递,或者在rejected action中作为error传递。在该函数内部,您可以使用async/await或Promise来执行异步操作。在该函数内部,可以使用dispatch和getState来进行Redux store中的状态更新和查询。

import { createAsyncThunk } from '@reduxjs/toolkit';
import { fetchUserById } from '../api';

export const fetchUser = createAsyncThunk('users/fetchUserById', async (userId) => {
  const response = await fetchUserById(userId);
  return response.data;
});

在这个例子中,定义了一个名为fetchUser的异步action creator。createAsyncThunk函数将自动生成三个action types,包括users/fetchUserById/pending、users/fetchUserById/fulfilled、users/fetchUserById/rejected。async callback函数将使用传入的userId参数执行异步操作,并将响应数据作为payload返回。

最后总结

Redux是一个状态管理库,它的核心思想是将整个应用程序的状态存储在一个单一的store中,并使用纯函数reducer来更新状态。但是,使用Redux本身需要编写大量与业务逻辑无关代码,特别是在涉及到定义action types和action creators时。

为了简化Redux的使用,社区提供了一些库,如 react-reduxRedux Toolkit

React-Redux是一个与React集成的Redux绑定库,它提供了一种简化Redux使用的方法。它提供了一个Provider组件,它将Redux store传递给整个React应用程序,并提供了一个connect函数,它允许组件连接到store并使用它的状态和action creator函数。

Redux Toolkit是一个由Redux官方维护的库,它旨在简化Redux的使用。它提供了一些常用的功能,如createSlice和createAsyncThunk函数,它们可以用来快速创建reducer和异步action creator函数。此外,它还提供了一个configureStore函数,它将自动配置多个Redux中间件,并使其易于使用。Redux Toolkit还提供了一些性能优化,如Immer库,它允许我们以可变方式更新状态,而不需要手动编写深层嵌套的不可变代码。

因此,从Redux到React-Redux再到Redux Toolkit可以帮助我们更轻松地管理应用程序状态,减少非业务逻辑代码的编写,提高应用程序的可维护性和性能。