🎯 掌握Redux核心:三大原则+实战代码,轻松驾驭状态管理

56 阅读12分钟

深入浅出Redux:从原理到实践

引言

在现代前端开发中,状态管理是一个不可避免的话题。随着应用复杂度的提升,如何高效、可预测地管理应用状态变得尤为重要。Redux作为一个流行的JavaScript状态管理库,以其“单一数据源”、“状态只读”和“纯函数修改”三大原则,为我们提供了一套清晰的状态管理范式。本文将从Redux的核心概念出发,结合其工作原理图,逐步深入到Redux的使用流程、中间件以及最佳实践,帮助你全面理解并掌握Redux。

一、Redux的基本概念 ✨

1. 什么是Redux?

Redux是一个可预测的状态容器,用于JavaScript应用。它帮助你编写行为一致的、可在不同环境(客户端、服务器、原生应用)中运行的、易于测试的应用。此外,它还提供了出色的开发体验,例如时间旅行调试和热模块重载。

Redux的核心思想是将整个应用的状态存储在一个单一的、不可变的对象树中,这个对象树被称为Store。当状态需要更新时,你不能直接修改Store,而是需要通过dispatch一个Action来描述发生了什么,然后由Reducer根据Action的类型来计算出新的状态。

2. Redux的三大原则

Redux遵循以下三大基本原则,这些原则是其可预测性和可维护性的基石:

(1) 单一数据源 (Single Source of Truth)

整个应用的状态都存储在一个单一的Store中。这意味着你不需要在多个地方管理状态,所有的数据都集中在一个地方,方便调试和维护。这就像一个大型应用的中央大脑,所有信息都汇聚于此。

(2) 状态只读 (State is Read-only)

Store中的状态是只读的,你不能直接修改它。唯一改变状态的方法是dispatch一个ActionAction是一个描述发生了什么的普通JavaScript对象。这种机制确保了状态的改变是可预测的,并且可以追踪。

例如,一个简单的Action可能看起来像这样:

{
  type: 'ADD_TODO',
  text: '学习Redux'
}
(3) 使用纯函数来执行修改 (Changes are Made with Pure Functions)

为了描述Action如何改变状态树,你需要编写纯函数ReducersReducer接收旧的StateAction作为参数,并返回一个新的State,而不是直接修改旧的State。纯函数意味着给定相同的输入,它总是返回相同的输出,并且没有副作用。

下面是一个简单的Reducer示例:

function todoReducer(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
}

这三大原则共同构成了Redux的核心思想,使得状态管理变得可预测、可调试和易于维护。

二、Redux的核心组件 ⚙️

Redux的核心由三个主要组件构成:StoreActionReducer。理解它们之间的协同工作是掌握Redux的关键。

redux原理图.png

1. Store

Store是Redux应用中唯一的状态树。它有以下几个职责:

  • 持有应用的状态:整个应用的状态都存储在一个JavaScript对象中。
  • 提供getState()方法:允许你获取当前的状态树。
  • 提供dispatch(action)方法:允许你更新状态。这是触发状态改变的唯一途径。
  • 提供subscribe(listener)方法:允许你注册监听器,当状态发生变化时,这些监听器会被调用。
  • 处理注销监听器subscribe方法返回一个函数,调用这个函数可以注销监听器。

你可以使用Redux库中的createStore方法来创建Store

import { createStore } from 'redux';

// 假设我们有一个todoReducer
const store = createStore(todoReducer);

console.log(store.getState()); // 获取初始状态

// 订阅状态变化
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

// 稍后可以取消订阅
// unsubscribe();
2. Action

Action是把数据从应用传到Store的有效载荷。它是Redux中唯一的数据源。Action是普通的JavaScript对象,它们必须有一个type字段来表示它们所执行的动作的类型。通常,type会被定义为字符串常量。

除了type字段外,Action对象可以包含任何你想要的数据。例如:

// 添加待办事项的Action
const addTodoAction = {
  type: 'ADD_TODO',
  id: 0,
  text: '学习Redux'
};

// 切换待办事项完成状态的Action
const toggleTodoAction = {
  type: 'TOGGLE_TODO',
  id: 0
};

为了方便管理和创建Action,我们通常会定义Action Creator函数,这些函数返回一个Action对象:

function addTodo(text) {
  return {
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
  };
}

function toggleTodo(id) {
  return {
    type: 'TOGGLE_TODO',
    id
  };
}

let nextTodoId = 0;

// 使用Action Creator
store.dispatch(addTodo('学习Redux'));
store.dispatch(toggleTodo(0));
3. Reducer

Reducer是纯函数,它接收当前的StateAction作为参数,并返回一个新的StateReducer是Redux中唯一可以修改状态的地方,但它不是直接修改,而是返回一个新的状态对象。

Reducer必须满足以下条件:

  • 纯函数:不应该有副作用,不应该修改传入的参数。
  • 给定相同的输入,总是返回相同的输出
  • 处理未知的Action类型时,返回当前的State

一个典型的Reducer结构会使用switch语句来根据Actiontype来处理不同的逻辑:

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    case 'TOGGLE_TODO':
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
}

// 另一个Reducer,例如处理可见性过滤器
function visibilityFilter(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter;
    default:
      return state;
  }
}

// 当应用有多个Reducer时,可以使用combineReducers将其组合起来
import { combineReducers } from 'redux';

const todoApp = combineReducers({
  todos,
  visibilityFilter
});

// 此时的state结构会是 { todos: [], visibilityFilter: 'SHOW_ALL' }

通过StoreActionReducer的协同工作,Redux实现了可预测的状态管理流程。当一个Actiondispatch时,它会经过Reducer处理,生成新的状态,然后Store会通知所有订阅者状态已更新。

三、Redux的使用流程 🔄

理解了Redux的核心概念和组件后,我们来看看如何在实际项目中应用Redux。以下是一个典型的Redux使用流程:

1. 安装 Redux 及其 React 绑定

首先,你需要在你的项目中安装Redux和React-Redux。React-Redux是官方提供的React绑定库,它使得React组件能够方便地与Redux Store进行交互。

npm install redux react-redux
# 或者
yarn add redux react-redux
2. 创建 Action

Action是描述发生了什么的普通JavaScript对象。为了更好地组织代码,我们通常会将Actiontype定义为常量,并创建Action Creator函数来生成Action

创建一个actions/index.js文件:

// actions/index.js

export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';

let nextTodoId = 0;

export const addTodo = text => ({
  type: ADD_TODO,
  id: nextTodoId++,
  text
});

export const toggleTodo = id => ({
  type: TOGGLE_TODO,
  id
});

export const setVisibilityFilter = filter => ({
  type: SET_VISIBILITY_FILTER,
  filter
});

export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
};
3. 创建 Reducer

Reducer是纯函数,它根据Action的类型来处理状态的更新。通常,我们会为应用中的每个状态切片创建一个Reducer,然后使用combineReducers将它们组合起来。

创建一个reducers/todos.js文件:

// reducers/todos.js

import { ADD_TODO, TOGGLE_TODO } from '../actions';

const todos = (state = [], action) => {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    case TOGGLE_TODO:
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
};

export default todos;

创建一个reducers/visibilityFilter.js文件:

// reducers/visibilityFilter.js

import { SET_VISIBILITY_FILTER, VisibilityFilters } from '../actions';

const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return action.filter;
    default:
      return state;
  }
};

export default visibilityFilter;

创建一个reducers/index.js文件来组合所有的Reducer

// reducers/index.js

import { combineReducers } from 'redux';
import todos from './todos';
import visibilityFilter from './visibilityFilter';

export default combineReducers({
  todos,
  visibilityFilter
});
4. 创建 Store

Store是Redux应用的核心,它将ActionReducer连接起来。使用createStore方法并传入根Reducer来创建Store

在你的应用入口文件(例如index.jsApp.js)中:

// index.js (或 App.js)

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

const store = createStore(rootReducer);

// 你可以在这里进行一些初始状态的设置或者订阅状态变化
// console.log(store.getState());
// store.subscribe(() => console.log(store.getState()));
5. 在 React 应用中使用 Store

为了让React组件能够访问到Redux Store,我们需要使用react-redux提供的Provider组件。Provider组件接收store作为prop,并将其传递给其子组件。

在你的应用入口文件(例如index.js)中:

// index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';
import App from './components/App'; // 你的根React组件

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
6. 连接 React 组件与 Redux

react-redux提供了connect高阶组件(HOC)或useSelectoruseDispatch Hooks来连接React组件与Redux Store。这里我们主要介绍Hooks的方式,因为它是目前更推荐的用法。

使用 useSelector 获取状态:

useSelector Hook允许你从Redux Store中提取数据。它接收一个选择器函数作为参数,该函数接收整个Redux Store状态作为参数,并返回你需要的状态切片。

// components/TodoList.js

import React from 'react';
import { useSelector } from 'react-redux';
import Todo from './Todo';

const TodoList = () => {
  const todos = useSelector(state => state.todos);
  const visibilityFilter = useSelector(state => state.visibilityFilter);

  const getVisibleTodos = (todos, filter) => {
    switch (filter) {
      case 'SHOW_ALL':
        return todos;
      case 'SHOW_COMPLETED':
        return todos.filter(t => t.completed);
      case 'SHOW_ACTIVE':
        return todos.filter(t => !t.completed);
      default:
        throw new Error('Unknown filter: ' + filter);
    }
  };

  const visibleTodos = getVisibleTodos(todos, visibilityFilter);

  return (
    <ul>
      {visibleTodos.map(todo => (
        <Todo key={todo.id} {...todo} />
      ))}
    </ul>
  );
};

export default TodoList;

使用 useDispatch 派发 Action:

useDispatch Hook返回Redux Store的dispatch函数,你可以使用它来派发Action

// components/AddTodo.js

import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from '../actions';

const AddTodo = () => {
  const [input, setInput] = useState('');
  const dispatch = useDispatch();

  const handleSubmit = e => {
    e.preventDefault();
    if (!input.trim()) {
      return;
    }
    dispatch(addTodo(input));
    setInput('');
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={e => setInput(e.target.value)} />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
};

export default AddTodo;

通过以上步骤,你就可以在React应用中成功地集成和使用Redux进行状态管理了。

四、Redux中间件 🔌

Redux中间件提供了一种拦截dispatch Action的机制。在Action到达Reducer之前,中间件可以执行异步操作、日志记录、错误报告等。最常用的中间件之一是redux-thunk,它允许你编写返回函数的Action Creator,而不是普通的Action对象。

1. redux-thunk

默认情况下,Redux的Action必须是普通JavaScript对象。这意味着你不能在Action Creator中执行异步操作,例如API请求。redux-thunk解决了这个问题,它允许Action Creator返回一个函数,这个函数接收dispatchgetState作为参数。

安装 redux-thunk

npm install redux-thunk
# 或者
yarn add redux-thunk

应用 redux-thunk

在创建Store时,你需要使用applyMiddleware来应用redux-thunk

// index.js (或 App.js)

import { createStore, applyMiddleware } from 'redux';
import { thunk } from 'redux-thunk'; // 注意:在Redux Toolkit中,thunk是默认包含的
import rootReducer from './reducers';

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

使用 redux-thunk 进行异步操作:

现在,你可以编写一个返回函数的Action Creator来处理异步逻辑。例如,一个模拟API请求的Action Creator

// actions/index.js

export const FETCH_POSTS_REQUEST = 'FETCH_POSTS_REQUEST';
export const FETCH_POSTS_SUCCESS = 'FETCH_POSTS_SUCCESS';
export const FETCH_POSTS_FAILURE = 'FETCH_POSTS_FAILURE';

export const fetchPosts = () => {
  return async (dispatch, getState) => {
    dispatch({ type: FETCH_POSTS_REQUEST });
    try {
      const response = await fetch("https://jsonplaceholder.typicode.com/posts");
      const data = await response.json();
      dispatch({ type: FETCH_POSTS_SUCCESS, payload: data });
    } catch (error) {
      dispatch({ type: FETCH_POSTS_FAILURE, error: error.message });
    }
  };
};

fetchPosts这个Action Creatordispatch时,redux-thunk会拦截它,并执行返回的函数。在这个函数中,你可以执行异步操作,并在操作完成后dispatch其他普通的Action来更新状态。

五、Redux的最佳实践 ✅

为了更好地维护和扩展Redux应用,以下是一些推荐的最佳实践:

1. 将代码模块化

随着应用规模的增长,将ActionReducerSelector等相关代码组织在一起变得非常重要。推荐使用“Ducks”模式或“Redux Toolkit”来组织代码。

Ducks 模式:

Ducks模式建议将ReducerAction TypeAction Creator定义在同一个文件中。这样,当你需要修改某个功能时,所有相关的代码都在一个地方,提高了代码的可维护性。

例如,一个todos.js文件可能包含:

// features/todos/todosSlice.js (Ducks 模式的简化版)

const ADD_TODO = 'todos/addTodo';
const TOGGLE_TODO = 'todos/toggleTodo';

export const addTodo = text => ({ type: ADD_TODO, payload: text });
export const toggleTodo = id => ({ type: TOGGLE_TODO, payload: id });

const initialState = [];

export default function todosReducer(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return [...state, { id: Date.now(), text: action.payload, completed: false }];
    case TOGGLE_TODO:
      return state.map(todo =>
        todo.id === action.payload ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
}

Redux Toolkit:

Redux Toolkit是官方推荐的Redux开发工具集,它简化了Redux的开发流程,内置了redux-thunkimmer等常用库,并提供了createSlice等API来帮助你更好地组织代码,遵循Ducks模式。

// features/todos/todosSlice.js (使用 Redux Toolkit)

import { createSlice } from '@reduxjs/toolkit';

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      state.push({ id: Date.now(), text: action.payload, completed: false });
    },
    toggleTodo: (state, action) => {
      const todo = state.find(todo => todo.id === action.payload);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
  },
});

export const { addTodo, toggleTodo } = todosSlice.actions;

export default todosSlice.reducer;
2. 使用组合 Reducers

当你的应用状态变得复杂时,将一个大的Reducer拆分成多个小的Reducer,每个Reducer负责管理应用状态树的一部分,然后使用combineReducers将它们组合起来,这是Redux推荐的模式。这使得每个Reducer都保持小巧、可维护,并且职责单一。

// reducers/index.js

import { combineReducers } from 'redux';
import todosReducer from '../features/todos/todosSlice'; // 假设你使用了Redux Toolkit
import usersReducer from '../features/users/usersSlice';

const rootReducer = combineReducers({
  todos: todosReducer,
  users: usersReducer,
  // ...其他reducer
});

export default rootReducer;
3. 使用 Selector

Selector是用于从Redux Store中提取数据的函数。它们可以封装状态访问逻辑,使得组件不需要直接了解状态树的结构。使用Selector的好处包括:

  • 提高可复用性:多个组件可以共享同一个Selector来获取相同的数据。
  • 简化组件逻辑:组件只需要调用Selector,而不需要编写复杂的逻辑来从状态中提取数据。
  • 优化性能:结合reselect库,Selector可以实现记忆化(memoization),避免不必要的重新计算,从而提高性能。
// selectors/todoSelectors.js

import { createSelector } from 'reselect';

const getTodos = state => state.todos;
const getVisibilityFilter = state => state.visibilityFilter;

export const getVisibleTodos = createSelector(
  [getTodos, getVisibilityFilter],
  (todos, visibilityFilter) => {
    switch (visibilityFilter) {
      case 'SHOW_ALL':
        return todos;
      case 'SHOW_COMPLETED':
        return todos.filter(todo => todo.completed);
      case 'SHOW_ACTIVE':
        return todos.filter(todo => !todo.completed);
      default:
        return todos;
    }
  }
);

// 在组件中使用
// const visibleTodos = useSelector(getVisibleTodos);

通过遵循这些最佳实践,你可以构建出更健壮、更易于维护和扩展的Redux应用。

总结

Redux提供了一个强大且可预测的状态管理方案,尤其适用于大型和复杂的前端应用。通过遵循其“单一数据源”、“状态只读”和“纯函数修改”三大原则,并结合StoreActionReducer这三大核心组件,我们可以构建出易于理解、测试和维护的应用。同时,利用中间件(如redux-thunk)处理异步操作,以及遵循模块化、组合Reducer和使用Selector等最佳实践,能够进一步提升开发效率和代码质量。

希望本文能帮助你深入理解Redux的原理和实践,并在你的前端开发旅程中有所助益。