前端状态管理:从基础方法到 Redux 实践前端状态管理:从基础方法到 Redux 实践 🤔在前端开发中,状态管理至关
前端状态管理:从基础方法到 Redux 实践
🤔在前端开发中,状态管理至关重要,涉及如何高效地维护和响应应用数据。本文探讨了基础状态管理工具(如 Redux、MobX、Vuex)及其在 React 中的应用,系统地介绍了从数据共享和变化感知到 UI 更新的实现方式,并详细演示了从基础状态管理到复杂的 Redux 实践过程。
什么是状态管理?
状态
前端开发可以被视为一个状态机的管理过程。在现代 web 工程中,我们更关注的是如何有效地管理和维护状态,而不仅仅是关注各个操作的执行过程。这种状态驱动的思维方式,使得我们能够更加灵活地响应用户的交互,并提升应用的可维护性和可扩展性。
在前端开发中,状态指的是在不同时间点上,应用当前的数据和待展示的内容。换句话说,前端的视图(View)和模型(Model)在不同的时刻体现了应用的状态。
管理
在 Web 应用的生命周期中,我们如何管理当前的数据模型(Model)?我们需要明确其作用范围,并理解它如何影响呈现的视图(View)。
软件工程的本质是什么? 它可以被归结为:软件工程的核心在于管理。这包括对需求、设计、开发、测试以及维护等各个阶段的系统化管理,以确保软件的质量、效率和可维护性。
在设计一个系统时,首要考虑的是以下几个方面:
- 系统中涉及哪些数据?
- 这些数据的生命周期是怎样的?
- 数据的作用范围和影响是什么?
数据存储和管理的位置
从持久化存储到临时存储的不同数据存储层次,以及如何在组件中管理数据状态:
- 数据库(DataBase) :持久化存储,应用级别
- 本地存储(localStorage) :持久化存储,客户端级别
- Cookie / 会话存储(cookie / sessionStorage) :会话级存储,用户登录态
- 项目运行时内存(Project runtime memory) :临时存储,尚未发送的内容
- 组件状态(Component: [state, props, data]) :局部存储,组件级别
状态管理的思路
状态管理概述
我们通常所说的状态管理,如 Vuex、Redux 和 MobX,都是在项目运行时(project runtime)内存中进行的。这意味着这些状态管理工具的存储数据在页面刷新后会丢失。
- Redux 采用发布订阅模式来管理状态。
- MobX 使用 Proxy 对象来进行状态管理。
- Vuex 是 Vue.js 的官方状态管理库,基于响应式系统和集中式存储来管理应用状态,支持插件和模块化。
在处理数据存储时,我们需要确保数据在运行时不会被垃圾回收(GC) 。为了防止数据丢失,我们需要确保数据的引用持续存在。具体来说,数据可以存储在以下位置:
- 全局对象(window / global) :全局范围内的数据存储。
- 闭包(closure) :函数内部的局部状态,通过闭包可以保持数据的引用。
理解这些存储机制可以帮助我们更好地管理状态,并防止数据意外丢失。
状态管理实现思路
目的: 数据共享、感知变化、UI 更新。
组件之外的数据共享
- 闭包(Closure) :可以用来在函数作用域外部保持数据状态。
- 全局对象(window/global) :可以用来在全局范围内共享数据状态。
数据修改的感知
数据修改时,相关方需要能够感知到变化:
- 函数回调:通过执行一个回调函数来感知数据的变化。
- 发布订阅模式:如 Redux,通过事件发布和订阅机制来通知状态变化。
- Proxy 对象:如 MobX,通过 Proxy 代理来自动跟踪和响应数据的变化。
状态修改触发 UI 更新
当状态发生变化时,必须触发 UI 的更新:
- Redux:使用 useSelector、connect 等方法将状态映射到组件的 props。
- React:使用 setState、useState、forceUpdate 等方法来更新组件的状态并重新渲染 UI。
一个简单的 setState 的实现流程
实现一个简单的状态管理
实现了一个简单的数据管理系统,允许对数据进行订阅、修改和获取。提供了一个 createData 函数,用于创建一个具有订阅功能的数据对象。数据对象可以被订阅,当数据发生变化时,所有订阅的处理函数(handler)都会被调用。
data.js
// 创建一个数据管理对象
export const createData = function (initData) {
// 存储订阅的处理函数
const deps = []
// 初始化数据
let data = initData
// 订阅函数,添加处理函数到订阅列表
function subscribe(handler) {
// 将传入的 handler 函数加入到订阅列表 deps 中
deps.push(handler)
}
// 修改数据,并通知所有订阅者
function changeData(val) {
data = val
// 遍历所有订阅的处理函数并执行
deps.forEach((handler) => handler())
}
// 获取当前的数据
function getData() {
return data
}
// 返回具有订阅、数据修改和获取功能的对象
return {
subscribe,
changeData,
getData,
}
}
// 初始化数据对象
const initData = {
count: 1,
}
// 使用 createData 创建一个数据对象实例
export const dataObj = createData(initData)
这段代码展示了一个基本的观察者模式实现,通过 createData 函数创建的对象能够允许订阅者在数据变化时得到通知。这个模式非常适合需要实时更新视图的应用场景,比如 React 状态管理、Vue 的响应式系统等。
下面展示了如何在 React 组件中使用上面定义的数据管理系统。
DataIndex.jsx
import React, { useEffect, useState } from 'react'
import { dataObj } from './data'
// 主组件,渲染 Child1 和 Child2
export default function DataIndex(props) {
return (
<div>
<Child1 />
<Child2 />
</div>
)
}
// Child1 组件,订阅数据变化
const Child1 = () => {
const [count, setCount] = useState() // 初始化状态,默认值未设置
useEffect(() => {
// 组件挂载时订阅数据变化
dataObj.subscribe(() => {
// 当数据改变时,获取最新数据并更新状态
let currentData = dataObj.getData()
setCount(currentData.count)
})
// 清理订阅函数,避免内存泄漏
return () => {
// 清理代码在这里添加
}
}, [])
// 渲染当前的 count 值
return <div>child1 中订阅的数据 count 为 {count}</div>
}
// Child2 组件,提供修改数据的按钮
const Child2 = () => {
return (
<div>
<button onClick={() => dataObj.changeData({ count: Math.random() })}>
random
</button>
</div>
)
}
存在问题
在跨组件传递数据时,直接使用 changeData 方法进行状态更新存在潜在的问题。例如,当我们在 dataObj 中修改 count 属性时,如果数据对象中还包含其他属性,这些属性可能会丢失。这样一来,状态管理的可靠性就会受到影响,因为我们只能部分更新数据,导致其他属性丢失。
为了避免这种问题,可以考虑将 useState 替换为 useReducer。useReducer 可以更好地处理复杂的状态更新逻辑,特别是在需要更新多个属性时。
SAFE BY ACTION
Action
Action 是用来描述状态变化的对象,类似于 useReducer 中的 action。通常包含两个部分:
- type:描述动作的类型。
- payload:包含要更新的数据。
示例:
const addAction = {
type: "ADD_ACTION",
payload: null,
}
const changeAction = {
type: "CHANGE_ACTION",
payload: 12,
}
Reducer
Reducer 是一个函数,接受当前的 state 和 action 作为参数,并返回一个新的 state。它负责根据 action 类型计算出更新后的状态。
状态管理系统
export const createData = function (reducer, initData) {
const deps = [] // 存储订阅的处理函数
let data = initData // 初始化数据
function subscribe(handler) {
// 订阅数据变化的处理函数
deps.push(handler)
}
function UNSAFE_changeData(val) {
// 不安全的直接修改数据
data = val
deps.forEach((handler) => handler())
}
function getData() {
return data
}
function changeDataByAction(action) {
// 使用 reducer 处理 action 并更新数据
data = reducer(data, action)
deps.forEach((handler) => handler())
}
return {
subscribe,
UNSAFE_changeData,
getData,
changeDataByAction,
}
}
// 定义 reducer 函数,处理不同类型的 action
const reducer = (data, action) => {
switch (action.type) {
case 'ADD_ACTION':
return { count: data.count + 1 }
case 'CHANGE_ACTION':
return { count: action.payload }
default:
return data
}
}
const initData = {
count: 1,
}
// 创建数据对象实例
export const dataObj = createData(reducer, initData)
React 组件示例
import React, { useEffect, useState } from 'react'
import { dataObj } from './data'
// 主组件,渲染 Child1 和 Child2
export default function DataIndex(props) {
return (
<div>
<Child1 />
<Child2 />
</div>
)
}
// Child1 组件,订阅数据变化
const Child1 = () => {
const [count, setCount] = useState(dataObj.getData().count)
useEffect(() => {
// 订阅数据变化
const handleDataChange = () => {
let currentData = dataObj.getData()
setCount(currentData.count)
}
dataObj.subscribe(handleDataChange)
// 清理订阅(如果需要)
return () => {
// 取消订阅逻辑(需要在 dataObj 中实现取消订阅的方法)
}
}, [])
return <div>child1 中订阅的数据 count 为 {count}</div>
}
// Child2 组件,提供修改数据的按钮
const Child2 = () => {
const addAction = {
type: 'ADD_ACTION',
payload: null,
}
const changeAction = {
type: 'CHANGE_ACTION',
payload: 12,
}
return (
<div>
<button onClick={() => dataObj.changeDataByAction(addAction)}>
Increment Count
</button>
<button onClick={() => dataObj.changeDataByAction(changeAction)}>
Set Count to 12
</button>
</div>
)
}
通过引入 action 和 reducer 的机制,我们可以更安全和更可预测地更新状态。useReducer 模式使得状态管理更加稳定,避免了直接修改状态带来的问题。这个方案不仅提高了跨组件数据传递的可靠性,还使得状态更新逻辑更加清晰和易于维护。
Redux
在上述实现中,我们使用了 action 和 reducer 来管理状态,这与 Redux 的核心理念非常相似。Redux 是一个流行的状态管理库,它也基于 action 和 reducer 的概念,通过集中式存储和严格的状态管理模式来提升应用的可维护性和可预测性。
Redux 的核心概念包括:
- Store:集中管理应用的状态。
- Action:描述应用中发生的事件。
- Reducer:处理 action 并返回新的状态。
- Dispatch:触发 action 并通知 reducer 更新状态。
- Selector:从 store 中选择状态并将其传递给组件。
Redux 提供了一种结构化和可扩展的方法来管理应用状态,特别适合于中大型应用中的复杂状态管理需求。
接下来,我们将深入探讨如何在 React 应用中集成 Redux,了解它如何与我们之前讨论的 action 和 reducer 机制结合,进一步提升状态管理的能力和效率。
React 应用中集成 Redux
安装 RTK 和 react-redux
这两个库分别用于状态管理和与 React 集成
pnpm install react-redux @reduxjs/toolkit
创建 Redux Store
创建一个名为 src/store/index.js 的文件。
通过 Redux Toolkit 的 configureStore API 创建了一个 Redux store,并将两个不同的 reducer 模块(counterReducer 和 channelReducer)合并到 store 中。counterReducer 处理与计数器相关的状态,而 channelReducer 处理与频道相关的状态。创建的 store 自动配置了 Redux DevTools 扩展,方便在开发过程中检查和调试状态。最后,store 被导出以供应用程序的其他部分使用。
// 从 Redux Toolkit 中导入 configureStore API
import { configureStore } from "@reduxjs/toolkit";
// 导入不同的 reducer 模块
import counterReducer from "./modules/counterStore"; // 负责处理计数器相关的状态
import channelReducer from "./modules/channelStore"; // 负责处理频道相关的状态
// 使用 configureStore 创建 Redux store
const store = configureStore({
reducer: {
// 定义 store 的 reducer
counter: counterReducer, // 处理与计数器相关的状态更新
channel: channelReducer, // 处理与频道相关的状态更新
},
});
// 导出创建的 store
export default store;
导入 configureStore API: configureStore 函数接受一个配置对象,其中 reducer 属性定义了一个对象,该对象将多个 reducer 组合在一起,每个 reducer 管理 store 中的不同部分的状态。
导入 reducer 模块:: 这些模块包含处理特定状态逻辑的 reducer 函数。counterReducer 处理计数器的状态,channelReducer 处理频道的状态。
创建 Redux store: configureStore 是 Redux Toolkit 提供的 API,用于创建 Redux store,并自动配置一些功能,如 Redux DevTools。
导出 store: 将创建的 store 导出,以便在应用程序的其他部分中使用,例如在 React 组件中通过 Provider 提供给组件树。
这样,你就可以在应用中使用 store,通过 Provider 将它传递给 React 应用的根组件,实现全局状态管理。
分模块
./modules/channelStore.js
// 从 Redux Toolkit 中导入 createSlice API
import { createSlice } from "@reduxjs/toolkit";
// 导入 axios 库,用于发送 HTTP 请求(虽然在当前代码中未使用)
import axios from "axios";
// 创建一个名为 'channel' 的 slice,用于管理频道相关的状态
const channelStore = createSlice({
name: "channel", // slice 的名称,用于生成相关的 action 类型
initialState: {
channelList: [], // 初始状态,channelList 为空数组
},
reducers: {
// 定义一个 reducer,用于设置频道列表
setChannels(state, action) {
state.channelList = action.payload; // 更新 channelList 为 action.payload 的值
},
},
});
// 从 slice 中解构出 action 创建函数
const { setChannels } = channelStore.actions;
export { setChannels }; // 导出 setChannels 函数,以便在组件中派发
// 导出 slice 的 reducer,用于配置 Redux store
const channelReducer = channelStore.reducer;
export default channelReducer;
导入模块: createSlice 用于简化创建 Redux slice 的过程。axios 库用于发送 HTTP 请求,虽然在当前代码中并未使用。
定义 slice:
createSlice 函数创建一个包含 name、initialState 和 reducers 的 slice:
- name: slice 的名称(‘channel’)。
- initialState: slice 的初始状态,包含一个空的 channelList。
- reducers: 定义了一个 reducer 函数 setChannels,用于更新 channelList 的状态。
导出 action 和 reducer:
- setChannels: 从 slice 中提取的 action 创建函数,用于在组件中派发动作更新状态。
- channelReducer: 从 slice 中提取的 reducer 函数,用于配置 Redux store。
./modules/counterStore.js
// 从 Redux Toolkit 中导入 createSlice API
import { createSlice } from "@reduxjs/toolkit";
// 创建一个名为 'counter' 的 slice,用于管理计数器的状态
const counterStore = createSlice({
name: "counter", // slice 的名称,用于生成相关的 action 类型
initialState: {
count: 0, // 初始状态,计数器的初始值为 0
},
// 定义同步的 reducer 函数来修改状态
reducers: {
// 增加计数器的值
increase(state) {
state.count++;
},
// 减少计数器的值
decrease(state) {
state.count--;
},
// 设置计数器的值为指定的数值
addToNum(state, action) {
state.count = action.payload;
},
},
});
// 从 slice 中解构出 action 创建函数
const { increase, decrease, addToNum } = counterStore.actions;
export { increase, decrease, addToNum }; // 导出这些函数以便在组件中派发
// 从 slice 中获取 reducer 函数
const counterReducer = counterStore.reducer;
// 导出 reducer 函数,用于配置 Redux store
export default counterReducer;
使用 Provider 组件将 Store 传递给 React 应用
通过 Provider 组件将 Redux store 提供给整个应用,使得应用可以通过 Redux 管理全局状态。
src/index.js
// 从 React 库中导入 React 和 ReactDOM
import React from "react";
import ReactDOM from "react-dom/client";
// 导入根组件 App
import App from "./App";
// 导入配置好的 Redux store
import store from "./store";
// 从 react-redux 中导入 Provider 组件
import { Provider } from "react-redux";
// 获取 HTML 中的 root 元素
const root = ReactDOM.createRoot(document.getElementById("root"));
// 渲染应用到 root 元素
root.render(
<Provider store={store}> {/* 提供 Redux store 给应用 */}
<App /> {/* 根组件 */}
</Provider>,
);
连接 React 组件到 Redux Store
src/App.js
// 从 react-redux 中导入 useSelector 和 useDispatch 钩子
import { useSelector, useDispatch } from "react-redux";
// 从 Redux store 中导入 action 创建函数
import { decrease, increase, addToNum } from "./store/modules/counterStore";
// 从 React 中导入 useEffect 和 useState 钩子
import { useEffect, useState } from "react";
// 从 API 模块中导入异步操作函数
import { fetchChannelList } from "./api";
function App() {
// 使用 useSelector 钩子从 Redux store 中获取计数器的值和频道列表
const { count } = useSelector((state) => state.counter);
const { channelList } = useSelector((state) => state.channel);
// 使用 useDispatch 钩子获取 dispatch 函数
const dispatch = useDispatch();
// 使用 useEffect 钩子在组件挂载时触发异步数据加载
useEffect(() => {
// 派发异步操作以获取频道列表
dispatch(fetchChannelList());
}, [dispatch]); // 依赖数组中包含 dispatch,确保 dispatch 函数不会变
return (
<div className="App">
<div>
{/* 显示当前的计数器值 */}
<span>count: {count}</span>
{/* 点击按钮派发不同的 actions 更新计数器 */}
<button onClick={() => dispatch(increase())}>increase</button>-
<button onClick={() => dispatch(decrease())}>decrease</button>-
<button onClick={() => dispatch(addToNum(10))}>addToNum</button>-
<button onClick={() => dispatch(addToNum(120))}>addToNum</button>-
</div>
<ul>
{/* 显示频道列表 */}
{channelList.map((item, idx) => (
<li key={idx}>{item}</li>
))}
</ul>
</div>
);
}
export default App;
api.js
import axios from "axios";
import { setChannels } from "../store/modules/channelStore";
export const fetchChannelList = () => {
return async (dispatch) => {
const res = await axios.get("http://localhost:3004/channels");
dispatch(setChannels(res.data.data.channels));
};
};
这里展示了如何在 React 应用中使用 Redux 进行状态管理和数据处理。首先,通过 useSelector 钩子从 Redux store 中提取计数器值和频道列表,并使用 useDispatch 钩子来派发更新状态的 actions。组件在挂载时通过 useEffect 钩子触发异步数据加载(fetchChannelList)。用户可以通过点击按钮来增加、减少计数器值或设置指定的值,同时更新后的频道列表会显示在页面上。整体上,这段代码演示了如何在 React 组件中集成 Redux 进行高效的状态管理和数据交互。
总结
在这篇文章中,我们探讨了前端状态管理的基本概念及其在 React 应用中的实现。状态管理不仅涉及如何管理和维护应用中的数据,还涵盖了数据的生命周期和在不同存储层次的存储机制。文章通过实现一个简单的数据管理系统(包括订阅、数据修改和获取功能)和集成 Redux 的详细步骤,展示了如何在现代 Web 开发中有效地管理状态。
主要内容包括:
-
状态管理的概念:状态是应用在特定时间点的数据和视图内容。前端开发中的状态管理需要确保应用能有效响应用户交互并提升可维护性。
-
数据存储层次:从持久化存储(如数据库)到临时存储(如组件状态),不同的存储方式用于不同的需求。
-
状态管理的实现:
- 观察者模式和简单的数据管理系统(包括订阅和通知机制)。
- 使用 action 和 reducer 的机制来处理状态更新。
-
Redux 集成:
- 使用 Redux Toolkit 创建 store,并通过 createSlice 定义 reducer 和 action。
- 在 React 组件中通过 useSelector 和 useDispatch 钩子实现状态的访问和更新。
- 使用异步操作函数(如 fetchChannelList)来处理数据加载。
转载自:https://juejin.cn/post/7402328037382127652