# react-zustand **Repository Path**: ocean_vane/react-zustand ## Basic Information - **Project Name**: react-zustand - **Description**: react-zustand - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-12-21 - **Last Updated**: 2024-12-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # React Zustand Tutorial ## Zustand 基础应用 ### **第一部分:创建 Zustand Store** 首先,我们需要创建一个 `store` 来管理全局状态。我们将定义一个简单的 `bearStore`,其中包含: - `bears`: 用于存储熊的数量 - `increasePopulation`: 用于增加熊的数量 - `removeAllBears`: 用于移除所有的熊 #### **1.1 创建 bearStore** 在 `stores/bearStore.js` 中创建我们的 Zustand store: ```js import { create } from "zustand"; export const useBearStore = create((set) => ({ bears: 0, // 初始化熊的数量 increasePopulation: () => set((state) => ({ bears: state.bears + 1, // 每次调用增加熊的数量 })), removeAllBears: () => set({ bears: 0 }), // 重置熊的数量为0 })); ``` ##### 代码解释: - `create` 是 Zustand 的核心方法,用来创建一个 store。 - `set` 方法用来更新 store 中的状态。 - 我们定义了三个属性: - `bears`: 存储当前熊的数量,初始值为 `0`。 - `increasePopulation`: 一个方法,用来将 `bears` 增加 1。 - `removeAllBears`: 一个方法,用来将 `bears` 重置为 0。 --- ### **第二部分:在 React 组件中使用 Zustand Store** 现在我们已经创建了 store,接下来我们将使用 Zustand 在 React 组件中获取和更新这些状态。 #### **2.1 使用 BearBox 组件** 在 `components/BearBox.jsx` 中,我们需要读取 `bears` 状态,并使用 `increasePopulation` 和 `removeAllBears` 方法来增加熊的数量或移除所有熊。 ```js import { useBearStore } from "@/stores/bearStore"; const BearBox = () => { // 从 Zustand store 获取熊的数量和方法 const bears = useBearStore((state) => state.bears); const increasePopulation = useBearStore((state) => state.increasePopulation); const removeAllBears = useBearStore((state) => state.removeAllBears); return (

BearBox

{/* 显示当前熊的数量 */} BearBox {bears}
{/* 增加熊的按钮 */} {/* 移除所有熊的按钮 */}
); }; export default BearBox; ``` ##### 代码解释: - `useBearStore` 是我们创建的 Zustand store 的钩子。通过它,我们可以从 store 中获取 `bears` 的数量,并访问 `increasePopulation` 和 `removeAllBears` 方法。 - 组件内部显示当前的熊的数量,并提供两个按钮: - **Add Bear**: 点击后增加熊的数量。 - **Remove All Bears**: 点击后将熊的数量重置为 0。 --- ### **第三部分:连接 BearBox 组件到 App 组件** 接下来,我们将在 `App.jsx` 中引用 `BearBox` 组件,完成整个应用的结构。 #### **3.1 设置 App 组件** 在 `App.jsx` 中: ```js import BearBox from "@/components/BearBox"; const App = () => { return (
{/* 引用 BearBox 组件 */}
); }; export default App; ``` ##### 代码解释: - 我们将 `BearBox` 组件放入 `App.jsx` 中,并渲染到页面上。 --- ### **第四部分:总结与扩展** #### **4.1 总结** 在本教程中,我们学习了如何使用 Zustand 来创建一个简单的状态管理: - 使用 `create` 来定义 store 和状态管理方法。 - 使用 `useBearStore` 来在 React 组件中获取和更新状态。 - 创建一个简单的 UI 组件来与状态交互,增加熊的数量或移除所有熊。 ## **Zustand 状态管理教程:管理嵌套对象** 如何使用 **Zustand** 管理嵌套对象的状态,并在 React 组件中更新和显示这些状态。我们将创建一个例子,显示两种类型的猫(大猫和小猫)的数量,并提供按钮来增加它们的数量。 ### **第一部分:创建 Zustand Store 并管理嵌套对象状态** 首先,我们需要创建一个 `useCatStore`,用来管理两类猫的数量:大猫和小猫。由于 `cats` 是一个嵌套对象,我们需要使用 **对象扩展**(`...`)来避免直接修改原有的状态对象,从而确保状态的正确性。 #### **1.1 创建 useCatStore** 在 `stores/catStore.js` 文件中: ```js import { create } from "zustand"; // 创建 Zustand store export const useCatStore = create((set) => ({ // cats 是一个嵌套对象,包含 bigCats 和 smallCats cats: { bigCats: 0, smallCats: 0, }, // 更新大猫数量的方法 increaseBigCats: () => { set((state) => ({ cats: { ...state.cats, // 保留原有的 cats 对象 bigCats: state.cats.bigCats + 1, // 增加大猫数量 }, })); }, // 更新小猫数量的方法 increaseSmallCats: () => { set((state) => ({ cats: { ...state.cats, // 保留原有的 cats 对象 smallCats: state.cats.smallCats + 1, // 增加小猫数量 }, })); }, })); ``` ##### **代码解释**: - `cats` 是一个嵌套对象,包含了 `bigCats` 和 `smallCats` 两个属性,用来分别记录大猫和小猫的数量。 - `increaseBigCats` 和 `increaseSmallCats` 是更新状态的方法,它们通过 `set` 函数来更新状态,并且每次都返回一个新的对象。通过使用对象扩展(`...state.cats`),我们确保 `cats` 对象中的其他属性不变,仅更新对应的 `bigCats` 或 `smallCats`。 - 这种方式确保了我们不会直接修改原始状态对象,而是生成一个新的状态对象,以保持 Zustand 状态管理的可预测性和一致性。 ------ ### **第二部分:在 React 组件中使用 Zustand Store** 我们现在将创建一个 `CatBox` 组件,用来显示和更新猫的数量。`CatBox` 组件将从 `useCatStore` 中获取大猫和小猫的数量,并通过按钮来增加它们的数量。 #### **2.1 创建 CatBox 组件** 在 `components/CatBox.jsx` 文件中: ```js import { useCatStore } from "@/stores/catStore"; const CatBox = () => { // 从 Zustand store 中获取大猫和小猫的数量 const bigCats = useCatStore((state) => state.cats.bigCats); const smallCats = useCatStore((state) => state.cats.smallCats); // 获取更新大猫和小猫数量的方法 const increaseBigCats = useCatStore((state) => state.increaseBigCats); const increaseSmallCats = useCatStore((state) => state.increaseSmallCats); return (

CatBox

{/* 显示大猫和小猫的数量 */}

Big Cats: {bigCats}

Small Cats: {smallCats}

{/* 按钮用于增加大猫数量 */} {/* 按钮用于增加小猫数量 */}
); }; export default CatBox; ``` ##### **代码解释**: - `useCatStore` 用来从 Zustand store 中获取状态。通过传递回调函数,我们只获取 `cats.bigCats` 和 `cats.smallCats` 这两个状态值,避免不必要的重新渲染。 - `increaseBigCats` 和 `increaseSmallCats` 是更新 `cats` 对象中对应值的函数,当按钮被点击时,这些函数会触发状态更新。 - 在 JSX 中,我们渲染了两个段落来显示当前的大猫和小猫的数量,并提供了两个按钮:一个用于增加大猫数量,另一个用于增加小猫数量。 ------ ### **第三部分:连接组件并实现交互功能** 我们已经完成了 `CatBox` 组件的创建,接下来,我们只需要将它导入到 `App.jsx` 中,并在应用中渲染它。 #### **3.1 设置 App 组件** 在 `App.jsx` 文件中: ```js import CatBox from "@/components/CatBox"; const App = () => { return (
{/* 引入并渲染 CatBox 组件 */}
); }; export default App; ``` ##### **代码解释**: - 我们在 `App.jsx` 中导入 `CatBox` 组件,并将它渲染到页面上。 - 这样,当我们打开应用时,会看到 `CatBox` 组件显示大猫和小猫的数量,以及两个按钮来增加它们的数量。 ------ ### **第四部分:总结与扩展** #### **4.1 总结** 我们学会了如何使用 **Zustand** 来管理 React 中的嵌套对象状态。我们创建了一个包含两类猫(大猫和小猫)数量的状态,并提供了按钮来增加它们的数量。我们还使用了 Zustand 的 `set` 方法和对象扩展操作来确保状态更新的一致性。 #### **4.2 扩展** - **管理更复杂的嵌套状态**:可以在 `cats` 对象中添加更多的属性,例如 `catColors` 或 `catBreeds`,并通过类似的方法更新它们。 - 如何更新嵌套对象中的多个属性?你可以在 `set` 函数中更新多个属性,只需对每个属性使用扩展操作符(`...`)来避免直接修改原始对象。 - 如何优化性能,避免多次渲染?可以通过 `useCatStore` 中选择性地提取需要的状态值,避免不必要的组件重新渲染,特别是当嵌套对象较大时。 以下是基于你提供的 **Zustand + Immer** 结合使用的教程逐字稿,讲解如何使用 Immer 中间件来管理不可变的嵌套状态,并通过 React 组件来交互。 ------ ## **Zustand + Immer 状态管理教程:不可变状态和嵌套修改** 我们将学习如何结合 **Zustand** 和 **Immer** 来管理 React 应用中的不可变状态,特别是处理嵌套状态时的简便方法。我们将通过示例演示如何使用 `immer` 中间件简化状态管理,并使代码更加简洁和易于维护。 ------ ### **第一部分:Immer 中间件的介绍** **Immer** 是一个用于管理不可变状态的库。它通过使用 **代理** 来允许直接修改状态对象,而在底层它会自动生成新的不可变对象。 在 Zustand 中,`immer` 是一个中间件,它使得我们能够像普通对象一样修改嵌套状态,而不需要显式地使用 `Object.assign()` 或者扩展运算符来创建新对象。 #### **1.1 Immer 的基本工作原理** - **不可变性**:Immer 使得状态不可变。每次修改状态时,它会创建一个新的状态对象,而不是直接修改原有的状态。 - **嵌套修改**:当修改嵌套的对象时,我们可以直接对嵌套对象的属性进行修改,Immer 会自动管理对象的更新。 ------ ### **第二部分:在 Zustand 中集成 Immer** 我们首先通过安装 `zustand` 和 `zustand/middleware/immer` 来集成 Zustand 和 Immer。 #### **2.1 创建 Zustand Store** 在 `stores/catStore.js` 文件中: ```js import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; // 使用 Zustand 和 Immer 创建 store export const useCatStore = create( immer((set, get) => ({ cats: { bigCats: 0, smallCats: 0, }, // 修改大猫数量的方法 increaseBigCats: () => { set((state) => { // 使用 Immer 可以直接修改状态 state.cats.bigCats++; }); }, // 修改小猫数量的方法 increaseSmallCats: () => { set((state) => { // 使用 Immer 可以直接修改状态 state.cats.smallCats++; }); }, // 使用 get 获取当前状态并计算猫的总数 summary: () => { const total = get().cats.bigCats + get().cats.smallCats; return total; }, })) ); ``` ##### **代码解析**: - `immer` 中间件将整个 `set` 函数包裹,使得在函数内部直接修改状态成为可能。 - 我们不再需要返回新的状态对象。通过 Immer,状态会在内部被处理为不可变的对象。 - `increaseBigCats` 和 `increaseSmallCats` 方法通过 `set` 修改状态,而 `summary` 方法则通过 `get` 获取当前的状态并计算猫的总数。 ------ ### **第三部分:通过 Immer 修改嵌套状态** 当我们处理嵌套状态(如 `cats.bigCats` 和 `cats.smallCats`)时,使用 Immer 可以简化代码。我们不需要担心状态的深拷贝或不可变性,Immer 会自动处理。 #### **3.1 修改嵌套状态** 在 `increaseBigCats` 和 `increaseSmallCats` 方法中,我们直接通过 `state.cats.bigCats++` 和 `state.cats.smallCats++` 来修改嵌套对象的值。 ##### **为什么这样可以直接修改?** - **Immer 会自动代理对象**,允许我们像普通的对象那样修改它们,而不必手动创建新对象或复制嵌套结构。 - Immer 会记录我们对状态对象的所有修改,并最终返回一个新的、不可变的状态。 ------ ### **第四部分:在 React 组件中使用 Zustand Store** 现在,我们将创建一个 React 组件来展示如何使用 Zustand Store 中的状态和更新方法。我们将通过按钮增加大猫和小猫的数量,并显示总数量。 #### **4.1 创建 CatBox 组件** 在 `components/CatBox.jsx` 文件中: ```js import { useCatStore } from "@/stores/catStore"; const CatBox = () => { // 从 Zustand store 中获取大猫和小猫的数量 const bigCats = useCatStore((state) => state.cats.bigCats); const smallCats = useCatStore((state) => state.cats.smallCats); // 获取更新大猫和小猫数量的方法 const increaseBigCats = useCatStore((state) => state.increaseBigCats); const increaseSmallCats = useCatStore((state) => state.increaseSmallCats); // 获取猫的总数 const summary = useCatStore((state) => state.summary()); return (

CatBox

{/* 显示大猫和小猫的数量 */}

Big Cats: {bigCats}

Small Cats: {smallCats}

Summary: {summary}

{/* 按钮用于增加大猫数量 */} {/* 按钮用于增加小猫数量 */}
); }; export default CatBox; ``` ##### **代码解释**: - 我们通过 `useCatStore` 获取大猫和小猫的数量,并渲染到页面中。 - 使用 `increaseBigCats` 和 `increaseSmallCats` 方法分别增加大猫和小猫的数量。 - `summary` 方法计算并显示猫的总数。注意,这里我们通过 `useCatStore` 获取并调用 `summary()`,不需要传入 `state` 参数,因为 `get()` 方法自动传入当前状态。 ------ ### **第五部分:总结与扩展** #### **5.1 总结** 在本教程中,我们展示了如何将 **Immer** 中间件与 **Zustand** 配合使用,以简化嵌套状态的管理。通过 Immer,我们可以像直接修改普通对象一样修改嵌套对象的状态,同时保证状态的不可变性。 - Immer 如何处理嵌套对象的深度修改? - Immer 会自动代理对象和数组,即使是嵌套的对象也可以直接修改而无需手动深拷贝。 - 如何确保 Immer 状态的不可变性? - Immer 会保证我们只修改副本,而不会修改原始状态。我们无需手动处理不可变性,Immer 会自动处理。 ## **Zustand 中的重新渲染问题与优化** 我们将探讨如何在 **Zustand** 中处理状态的选择和组件的重新渲染问题。我们将通过使用两个组件 `CatBox` 和 `CatBox2` 来演示如何减少不必要的渲染,确保应用性能得到优化。 ------ ### **第一部分:问题的引入** 我们开始时,利用解构建直接从 Zustand store 中获取所有的状态和方法。这可能会导致组件的重新渲染,特别是当状态中的某个值发生变化时,所有使用这个 store 的组件都将重新渲染。 #### **1.1 `CatBox` 组件的实现** 在 `CatBox` 组件中,我们直接从 `useCatStore()` 中获取所有的状态和方法: ```js import { useCatStore } from "@/stores/catStore"; const CatBox = () => { const { cats: { bigCats, smallCats }, increaseBigCats, increaseSmallCats, summary, } = useCatStore(); return (

CatBox

Big Cats: {bigCats}

Small Cats: {smallCats}

Summary: {summary()}

Random:{Math.random()}

); }; export default CatBox; ``` ##### **分析:** - 在这个组件中,我们使用 `useCatStore()` 获取整个 `cats` 对象以及增加猫的函数。 - 每当 `cats.bigCats` 或 `cats.smallCats` 改变时,`CatBox` 组件会重新渲染,因为 `useCatStore()` 默认会订阅整个 store 的变化。 #### **1.2 `CatBox2` 组件的实现** 我们可以添加另一个组件 `CatBox2`,仅订阅 `bigCats` 的变化: ```js import { useCatStore } from "@/stores/catStore"; const CatBox2 = () => { const { cats: { bigCats }, } = useCatStore(); return (

CatBox

Big Cats: {bigCats}

Random:{Math.random()}

); }; export default CatBox2; ``` ##### **分析:** - 这个组件只会关注 `bigCats`,而忽略 `smallCats`。 - 由于它只订阅了 `bigCats`,当 `smallCats` 更新时,`CatBox2` 会不会重新渲染? #### **1.3 观察重新渲染** - 当我们点击按钮增加大猫或小猫的数量时,`CatBox` 和 `CatBox2` 是否都会重新渲染呢? - 为了验证这一点,我们可以在两个组件中添加一个 `Random` 字段,输出随机数 `Math.random()`,它会在每次重新渲染时更新。 ------ - **Q:为什么 `Math.random()` 每次渲染都会变化?** - A:`Math.random()` 是一个会生成随机数的函数,每次组件渲染时都会被重新执行,导致显示的随机数发生变化。 - **Q:如果多个组件共享同一个 Zustand store,如何避免它们之间的相互影响?** - A:通过选择器优化,可以确保每个组件只订阅自己关心的状态部分,从而避免其他部分的状态变化影响不需要更新的组件。 ## `createSelectors` 的使用原因 ------ 通过创建和使用 `createSelectors` 来提升组件性能,从头到尾分析为什么要使用 `createSelectors`,以及它如何帮助我们管理和选择状态。 ------ ### **状态管理的挑战:** 首先,大家可能知道,使用 Zustand 管理状态时,我们直接从 store 中获取数据。这样做的一个潜在问题就是:每次从 store 中获取状态时,整个组件会重新渲染。例如,在 `CatBox` 组件中,当我们直接从 store 获取 `bigCats` 或 `smallCats` 的值时,任何与这些状态相关的更改都会触发组件的重新渲染。 ```js const { cats: { bigCats, smallCats }, increaseBigCats, increaseSmallCats, summary } = useCatStore(); ``` 这种做法虽然能正常工作,但每次获取 `bigCats` 或 `smallCats` 时,都会导致整个组件重新渲染,即使组件中的某些部分并没有发生变化。这会影响性能,特别是在复杂应用中,组件数量较多的情况下。 ------ ### **如何解决这个问题:** 为了解决这个问题,我们可以引入 **selector**(选择器)来避免不必要的渲染。通过 selector,我们可以只选择并监听我们关心的状态部分,而不必每次都重新获取整个状态。这意味着,当我们只改变一个状态字段时,只有依赖该字段的组件会重新渲染,而不是整个组件。 这就是为什么我们要使用 `createSelectors` 的原因。它允许我们将每个状态字段封装成一个选择器,并通过这些选择器单独访问状态。 ------ ### **`createSelectors` 的实现:** 那么,`createSelectors` 是怎么工作的呢? ```js export const createSelectors = (_store) => { let store = _store; store.use = {}; // 遍历 store 的状态属性,将每个状态字段封装成一个选择器函数 for (let k of Object.keys(store.getState())) { store.use[k] = () => store((s) => s[k]); } return store; }; ``` 在这里,我们定义了一个 `createSelectors` 函数。这个函数接收一个 Zustand store 作为参数,遍历 store 中的所有状态字段,并为每个字段创建一个选择器函数。每个选择器都会返回该字段的最新值。 这样做的好处是,组件可以通过 `store.use` 来访问单个状态字段,而不需要每次重新获取整个 store 的状态。也就是说,`createSelectors` 让我们能够精确控制组件的渲染,只在需要时才重新渲染。 ------ ### **使用 `createSelectors`:** 接下来,让我们来看一下如何将 `createSelectors` 应用于我们的实际项目中。假设我们有一个 `useCatStore` 的状态管理: ```js export const useCatStore = createSelectors( create()( immer((set, get) => ({ cats: { bigCats: 0, smallCats: 0, }, increaseBigCats: () => { set((state) => { state.cats.bigCats++; }); }, increaseSmallCats: () => { set((state) => { state.cats.smallCats++; }); }, summary: () => { const total = get().cats.bigCats + get().cats.smallCats; return total; }, })) ) ); ``` 在这里,我们使用 `immer` 中间件来确保状态的不可变性,并通过 `createSelectors` 对状态进行封装。通过 `createSelectors`,我们可以访问 store 的各个字段,例如: - `useCatStore.use.cats.bigCats()` - `useCatStore.use.cats.smallCats()` 我们将 `bigCats` 和 `smallCats` 分别封装成了选择器。这样,只有当 `bigCats` 或 `smallCats` 发生变化时,依赖这些字段的组件才会重新渲染,从而提高了性能。 ------ ### **避免不必要的渲染:** 通过 `createSelectors` 的引入,我们避免了直接从 store 中获取整个状态,并且不会因为 `bigCats` 或 `smallCats` 的微小变化而导致整个组件的重新渲染。每个组件都可以通过只选择自己关心的状态部分来进行渲染,减少了不必要的性能开销。 例如,在 `CatBox2` 组件中,我们只关心 `bigCats`,因此我们通过选择器来精确获取 `bigCats` 的状态,而不是每次获取整个 `cats` 对象: ```js const bigCats = useCatStore.use.cats.bigCats(); ``` 这样,当 `smallCats` 的值变化时,`CatBox2` 不会重新渲染,只有依赖 `bigCats` 的部分会更新。 ------ ### **总结:** 我们了解了为什么要使用 `createSelectors` 以及它如何帮助我们优化状态管理。在 Zustand 中,默认情况下每次从 store 获取数据时都会触发组件的重新渲染,而 `createSelectors` 通过封装选择器函数,帮助我们避免不必要的渲染,并提高了组件的性能。 通过这种方法,我们可以更精确地控制状态的访问和组件的渲染。使用 `createSelectors`,我们能够确保只有依赖特定状态字段的组件才会更新,从而有效减少渲染次数,提升整体性能。 ## **使用 `useShallow` 进行浅比较优化** ### **背景问题:** 首先,我们需要了解在 Zustand 中,组件重新渲染的触发机制。通常,Zustand 会在状态变化时触发组件的重新渲染。每次调用 `useCatStore` 以获取新的状态时,整个组件都会重新渲染。例如: ```js const { bigCats, smallCats } = useCatStore(); ``` 这种做法的问题是,当 store 中的任何状态发生变化时,组件会触发重新渲染。即使某些字段没有改变,也会重新渲染整个组件。这对于性能要求较高的应用来说,可能会导致不必要的性能消耗。 ------ ### **浅比较的优势:** 为了解决这个问题,Zustand 提供了一个非常有用的功能:`useShallow`。`useShallow` 是一种优化手段,它通过执行浅比较(shallow comparison)来判断组件是否应该重新渲染。简单来说,只有当 `bigCats` 或 `smallCats` 发生变化时,`CatBoxController` 组件才会重新渲染,而不是每次状态变化时都触发渲染。 ------ ### **`useShallow` 的工作原理:** `useShallow` 在内部会对传入的对象进行浅比较。浅比较意味着它只会检查对象的第一层属性是否有变化,而不会递归比较对象中的嵌套属性。如果第一层属性没有变化,组件将不会重新渲染。 在下面的代码中,我们使用 `useShallow` 来优化我们的 `CatBoxController` 组件: ```js import { useCatStore } from "@/stores/catStore"; import { useShallow } from "zustand/shallow"; const CatBoxController = () => { const { increaseBigCats, increaseSmallCats, bigCats, smallCats } = useCatStore( useShallow((state) => { return { increaseBigCats: state.increaseBigCats, increaseSmallCats: state.increaseSmallCats, bigCats: state.cats.bigCats, smallCats: state.cats.smallCats, }; }) ); return (

CatBox

Big Cats: {bigCats}

Small Cats: {smallCats}

Random: {Math.random()}

); }; export default CatBoxController; ``` ------ ### **代码解析:** 在上面的代码中,我们通过 `useCatStore` 获取了 `bigCats` 和 `smallCats`,以及 `increaseBigCats` 和 `increaseSmallCats` 函数。然后我们将这些状态和函数传递给 `useShallow` 进行优化: 1. `useShallow` 接受一个回调函数作为参数,该回调函数从 `state` 中提取需要的字段。 2. `useShallow` 将会对 `bigCats` 和 `smallCats` 进行浅比较。如果 `cats.bigCats` 或 `cats.smallCats` 的值发生变化,组件将重新渲染。如果它们没有变化,组件就不会重新渲染。 3. 我们还传递了两个更新函数 `increaseBigCats` 和 `increaseSmallCats`,这些函数不受浅比较的影响,因为它们是引用类型,但它们会更新对应的状态。 ------ ### **性能优化:** 这种优化的核心在于 `useShallow`,它使得组件只有在我们关心的状态发生变化时才会重新渲染,而不需要每次状态变化时都进行全量的重新渲染。举个例子,如果 `smallCats` 的值发生了变化,`bigCats` 没有变化,`CatBoxController` 组件依然会正常渲染,但是 `CatBoxController` 的重渲染仅仅是因为 `smallCats` 发生了变化,而不是因为整个 store 的状态都变化了。 ------ ### **为什么 `useShallow` 有效?** `useShallow` 的优势在于它避免了深层次的比较。深层比较(deep comparison)是比较每一层对象的值,在嵌套对象较多时,深比较的性能开销非常大。通过浅比较,我们只关心最外层的状态变化,大大减少了计算量。 例如,假设我们的 `cats` 对象嵌套了更多的数据。如果没有 `useShallow`,每次状态变化都会触发深比较,这将导致性能瓶颈。通过 `useShallow`,只会检查 `bigCats` 和 `smallCats` 的变化,不会对其他部分进行比较,从而有效提升性能。 ------ ### **实际效果:** 在 `CatBoxController` 中,每次 `bigCats` 或 `smallCats` 变化时,只有相应的部分会重新渲染,其他未变化的部分(例如 `Random`)保持不变。这样,组件的性能得到了提升,尤其是在应用状态变得更加复杂时,浅比较的优势会更加明显。 ------ **总结:** 我们学习了如何通过 `useShallow` 来进行浅比较优化组件的渲染。使用 `useShallow` 可以帮助我们减少不必要的渲染,提高组件的性能,尤其是在状态更新较为频繁的情况下。通过浅比较,组件仅在关心的状态字段发生变化时才重新渲染,从而避免了全量的重新渲染和性能开销。 希望大家能够将 `useShallow` 应用到自己的项目中,优化组件的渲染性能。如果对 `useShallow` 或其他 Zustand 的性能优化有任何疑问,欢迎在下方留言,我们一起讨论! ## **关于 `devtools` 中间件的顺序与应用** ------ 我们将探讨如何在 Zustand 中使用 `devtools` 中间件,并了解为什么它应该放在 `immer` 中间件之后进行应用。理解中间件的执行顺序是非常关键的,它会直接影响到我们应用状态的管理与调试功能。 ------ ### **背景问题:** 首先,我们来简要回顾一下我们使用的两个中间件:`immer` 和 `devtools`。 - **`immer`** 是 Zustand 的一个中间件,它支持不可变性,并允许我们以更简洁的方式修改状态。通过 `immer`,我们可以像普通对象那样修改嵌套的状态,而不需要返回一个新的对象。它会自动将修改的对象转换为不可变的,从而避免直接修改原始状态。 - **`devtools`** 是 Zustand 另一个非常重要的中间件,它让我们能够在开发环境下使用浏览器的开发者工具来跟踪和调试状态。借助 `devtools`,我们可以在 React Developer Tools 或 Redux DevTools 中看到状态的变更,便于调试和优化我们的应用。 然而,`devtools` 和 `immer` 这两个中间件的顺序是非常关键的,我们需要确保它们的顺序放置正确,以免出现意料之外的行为。 ------ ### **问题的核心:** 为什么 `devtools` 必须放在 `immer` 之后呢?让我们来逐步分析。 1. `immer` 的工作原理: - `immer` 中间件接管了状态的更新,允许我们在修改状态时避免直接改变原始状态。它会使用代理来拦截状态的修改,并在修改完成后生成新的状态对象。这意味着,`immer` 会生成一个新的状态对象,并将其传递给 `set` 方法。 2. `devtools` 的工作原理: - `devtools` 中间件允许我们在开发者工具中查看状态的变化。它会记录每次状态变更,并通过浏览器的开发工具显示出来,帮助我们调试应用。`devtools` 需要捕获 `set` 的调用,以便能够正确记录每次状态的变化。 ------ ### **为什么要将 `devtools` 放在 `immer` 后面?** 如果我们将 `devtools` 放在 `immer` 之前,那么 `devtools` 捕获到的状态变更将会是错误的。原因在于: - `immer` 会在状态更新时生成一个新的状态对象,这个过程是在 `set` 被调用之前进行的。如果我们先应用 `devtools`,它就会捕获到 `set` 调用的原始状态,而不是 `immer` 处理后的新状态。这会导致我们在 `devtools` 中看到的状态变更不准确,调试工具无法正确显示更新后的状态。 - 而如果我们将 `devtools` 放在 `immer` 后面,`devtools` 会正确捕获到通过 `immer` 处理过的状态,即变更后的状态,这样我们就能在开发者工具中看到准确的状态变化。 ------ ### **正确的中间件顺序:** ```js import { create } from "zustand"; import { createSelectors } from "@/utils/createSelectors"; import { devtools } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; export const useCatStore = createSelectors( create()( immer( // Immer 放在前面,确保状态变得不可变 devtools((set, get) => ({ cats: { bigCats: 0, smallCats: 0, }, increaseBigCats: () => { set((state) => { state.cats.bigCats++; }); }, increaseSmallCats: () => { set((state) => { state.cats.smallCats++; }); }, summary: () => { const total = get().cats.bigCats + get().cats.smallCats; return total; }, })) ) ) ); ``` 在上面的代码中,我们确保了 `immer` 中间件应用在 `devtools` 中间件之前。这样,`immer` 会处理状态的不可变性和嵌套修改,而 `devtools` 会捕捉到最终的、已经更新的状态。 ------ ### **总结:** 今天我们学习了 Zustand 中间件的顺序问题,特别是 `immer` 和 `devtools` 的顺序为何至关重要。 - **`immer` 应该放在 `devtools` 之前。** 这是因为 `immer` 需要在状态更新之前处理状态的不可变性,而 `devtools` 需要捕捉最终的更新状态。 - 如果我们反过来使用 `devtools` 放在前面,`devtools` 将无法捕捉到正确的状态更新,影响调试和开发体验。 ## **关于 `persist` 和 `partialize` 的配置与使用,及其顺序问题** ------ 我们将讨论如何在 Zustand 中使用 `persist` 和 `partialize`,并重点讲解它们的配置方式和使用顺序。`persist` 是一个非常强大的中间件,它帮助我们将状态持久化到本地存储或其他存储介质中,以便在页面刷新后保持状态。我们还将介绍 `partialize`,它允许我们控制持久化时保存状态的哪些部分。 ------ ### **背景知识:** 首先,我们来了解一下 `persist` 和 `partialize` 是什么。 - **`persist`** 是 Zustand 的中间件之一,它提供了持久化存储的功能。它可以将应用的某些状态保存到本地存储、session 存储或其他存储介质中,确保应用在刷新页面时能够恢复之前的状态。 - **`partialize`** 是 `persist` 中的一项配置选项,用于筛选持久化存储的状态字段。通过 `partialize`,我们可以指定哪些状态需要持久化,哪些不需要保存。它通常用于排除某些敏感或不必要的数据。 在实际开发中,`persist` 和 `partialize` 的顺序是非常重要的,因为它们会影响状态的存储方式以及应用恢复时的表现。 ------ ### **`persist` 的使用和顺序问题:** 在使用 `persist` 时,我们需要确保它的顺序正确。一般来说,`persist` 应该放在最后,以确保它能够接收到完整的状态数据并将其持久化。如果把 `persist` 放在中间,可能会导致一些不必要的数据丢失或无法正确保存状态。 此外,`partialize` 是 `persist` 的一项配置,它允许我们精细控制需要持久化的状态部分。我们可以通过配置 `partialize` 来排除某些不需要保存的字段,比如临时数据或敏感数据。 ------ ### **代码解析:** 下面是一个使用 `persist` 和 `partialize` 的示例,我们会逐步解析其中的配置与顺序问题。 ```js import { devtools, persist } from "zustand/middleware"; import { create } from "zustand"; import { createSelectors } from "@/utils/createSelectors"; import { immer } from "zustand/middleware/immer"; export const useCatStore = createSelectors( create()( // immer 是zustand的中间件,用于支持状态的不可变性,以及支持状态的嵌套修改 immer( devtools( persist( (set, get) => ({ color: "red", // 此字段将不会持久化(在partialize中排除了) cats: { bigCats: 0, smallCats: 0, }, increaseBigCats: () => { set((state) => { state.cats.bigCats++; }); }, increaseSmallCats: () => { set((state) => { state.cats.smallCats++; }); }, // 状态的摘要,返回当前bigCats和smallCats的总数 summary: () => { const total = get().cats.bigCats + get().cats.smallCats; return total; }, }), { name: "cat-store", // 状态持久化的名称 partialize: (state) => // partialize 过滤掉color字段,避免将color持久化到存储中 Object.fromEntries( Object.entries(state).filter( ([key]) => !["color"].includes(key) ) ), } ) ) ) ) ); ``` ------ ### **关键点讲解:** 1. **`persist` 放置的位置:** - `persist` 必须放在 `immer` 和 `devtools` 中间件之后。因为 `immer` 用来处理不可变性,而 `devtools` 用来调试状态,`persist` 需要访问状态的完整数据并将其保存到存储中。`persist` 是最后执行的中间件,因此它能够持久化整个状态。 2. **`partialize` 配置:** - 在 `persist` 配置中,`partialize` 允许我们选择性地保存状态的部分字段。在这个例子中,`color` 字段被排除在持久化之外。因此,当应用状态被保存时,`color` 字段不会被存储到本地存储或其他存储介质中。 - `partialize` 使用 `Object.entries(state)` 将状态转为键值对数组,并通过 `filter` 函数排除不需要保存的字段(例如:`color`)。最终返回的对象中只包含需要持久化的部分。 3. **中间件的执行顺序:** - **`immer`** 负责处理状态的不可变性,它会将状态变更转化为不可变对象。它应该在 `devtools` 和 `persist` 之前执行。 - **`devtools`** 用于调试,在开发环境中启用开发者工具来跟踪状态的变化。它应在 `immer` 之后执行。 - **`persist`** 最后执行,它将处理状态的持久化操作,并确保数据被正确地存储。它会对 `immer` 和 `devtools` 中处理后的状态进行持久化。 ------ ### **总结:** 我们学习了如何使用 `persist` 和 `partialize` 中间件来持久化 Zustand 的状态,并确保它们的顺序配置正确。 - **`persist` 中间件的顺序至关重要**,它应放在所有其他中间件之后,以确保状态的正确持久化。 - **`partialize` 配置** 让我们能够选择性地持久化某些状态字段,从而避免不必要的数据存储。 - 理解这些中间件的顺序和配置,可以帮助我们在开发过程中保持高效且灵活的状态管理。 ## **关于 `subscribeWithSelector` 的作用与性能优化** Zustand 是一个非常灵活且轻量的状态管理库,它提供了一些中间件来增强我们的状态管理功能。其中,`subscribeWithSelector` 是一个非常有用的中间件,它不仅能够帮助我们精确选择需要的状态,还能够提高组件渲染的性能。 ------ ### **背景知识:** 首先,让我们了解一下 `subscribeWithSelector` 中间件的作用。它是 Zustand 的一个增强工具,通常用来提高组件渲染性能。默认情况下,Zustand 在订阅状态时,会重新渲染组件每当任何状态变化时。然而,`subscribeWithSelector` 使得我们可以仅根据特定的状态片段(selector)来选择性地订阅,而不是订阅整个状态对象。 通过这种方式,`subscribeWithSelector` 能够避免不必要的组件重新渲染,只触发状态变化的相关部分,而不影响其他部分。 ------ ### **`subscribeWithSelector` 的作用:** 在没有 `subscribeWithSelector` 时,Zustand 会在状态的任何部分发生变化时,触发所有使用该状态的组件重新渲染。这样虽然简单,但会造成性能浪费,特别是在大型应用中,频繁的组件重新渲染可能会影响用户体验。 通过 `subscribeWithSelector`,我们可以选择性地订阅状态的某些部分。这样,当我们只关心某些状态字段时,Zustand 就不会因为整个状态的变化而触发不必要的渲染。 例如,在我们的 `CatBoxController` 组件中,我们只关心 `bigCats` 的状态变化。使用 `subscribeWithSelector` 后,组件只会在 `bigCats` 改变时重新渲染,而不是每当整个 `cats` 对象发生变化时重新渲染。 ------ ### **代码解析:** 下面是代码示例,展示了如何使用 `subscribeWithSelector`: ```js import { devtools, persist, subscribeWithSelector } from "zustand/middleware"; import { create } from "zustand"; import { createSelectors } from "@/utils/createSelectors"; import { immer } from "zustand/middleware/immer"; export const useCatStore = createSelectors( create()( immer( devtools( subscribeWithSelector( // 使用 subscribeWithSelector 中间件 persist( (set, get) => ({ color: "red", cats: { bigCats: 0, smallCats: 0, }, increaseBigCats: () => { set((state) => { state.cats.bigCats++; }); }, increaseSmallCats: () => { set((state) => { state.cats.smallCats++; }); }, summary: () => { const total = get().cats.bigCats + get().cats.smallCats; return total; }, }), { name: "cat-store", partialize: (state) => Object.fromEntries( Object.entries(state).filter( ([key]) => !["color"].includes(key) ) ), } ) ) ) ) ) ); ``` 在这个例子中,我们使用了 `subscribeWithSelector` 来包裹 `persist` 中间件。它的作用是优化状态的订阅,使得只有在我们关注的状态字段发生变化时,组件才会重新渲染。 接下来,我们来看看 `CatBoxController` 组件中的具体实现: ```js import { useCatStore } from "@/stores/catStore"; const CatBoxController = () => { const bigCats = useCatStore((state) => state.cats.bigCats); // 仅订阅 bigCats 状态 const increaseBigCats = useCatStore((state) => state.increaseBigCats); const increaseSmallCats = useCatStore((state) => state.increaseSmallCats); return (

CatBox

Big Cats: {bigCats}

Random:{Math.random()}

); }; export default CatBoxController; ``` 在这段代码中,`useCatStore` 使用了一个 selector 来选择 `bigCats` 状态,这样 `CatBoxController` 组件只会在 `bigCats` 状态发生变化时重新渲染,而不是每次 `cats` 对象中的任何字段发生变化时都重新渲染。 ------ ### **性能优化:** `subscribeWithSelector` 可以显著优化性能,特别是在大型应用中。例如,在我们的应用中,可能会有多个组件都使用了 `cats` 对象中的状态,而我们只关心其中的一部分字段。如果没有 `subscribeWithSelector`,每当 `cats` 对象发生变化时,所有依赖于 `cats` 的组件都会重新渲染,这可能导致性能瓶颈。 通过使用 `subscribeWithSelector`,我们可以让组件只在需要的状态发生变化时才重新渲染,从而减少了不必要的渲染,提高了性能。 **总结:** 1. **`subscribeWithSelector` 的作用**:它允许我们只订阅状态中的特定部分,避免了不必要的组件重新渲染,从而优化了性能。 2. **性能优化**:通过使用 `subscribeWithSelector`,组件只会在关注的状态变化时重新渲染,避免了整个状态对象变化时的过度渲染,特别是在复杂的状态对象和大型应用中尤为重要。 3. **适用场景**:适用于需要精确控制组件渲染的场景,特别是在状态对象较大或状态变化频繁的情况下,`subscribeWithSelector` 可以有效减少性能损耗。 ## **数据与方法的外部抽离,提升代码的可维护性** ------ 我们将讨论如何通过外部抽离数据和方法来提升代码的可维护性。通过一个实际的例子,展示如何将状态管理逻辑与组件的显示逻辑解耦,使得代码更加清晰、易于维护。 在前端开发中,尤其是当应用规模增大时,数据和方法的管理变得尤为重要。为了保持代码的整洁和可维护性,我们通常需要将这些逻辑从组件中抽离出来,集中管理。 ------ ### **背景知识:** 我们常见的做法是,直接在组件内管理和操作状态。例如,在 React 或 Vue 中,组件通常会直接通过 `useState` 或 Vue 的 `ref`、`reactive` 等 API 来管理状态,并通过方法修改这些状态。然而,随着项目的复杂性增加,这种做法可能导致组件变得臃肿,难以维护。 因此,我们可以将状态和方法抽离到外部,例如使用 Zustand 等状态管理库,将其提取到独立的 store 中。这不仅使得组件的关注点更加单一,也方便了状态的管理和维护。 ------ ### **示例代码解析:** 我们来看看如何通过外部抽离数据和方法,来提升代码的可维护性。我们将以 Zustand 状态管理库为例,来进行演示。 首先是 `catStore`,它包含了 `cats` 相关的状态和方法。我们把状态和方法提取到外部,这样做的好处是,任何需要访问 `cats` 状态的组件都可以直接引用这个 store,而不需要关心具体的实现细节。 ```js import { devtools, persist, subscribeWithSelector } from "zustand/middleware"; import { create } from "zustand"; import { immer } from "zustand/middleware/immer"; const initialState = { color: "red", cats: { bigCats: 0, smallCats: 0, }, }; export const useCatStore = create( immer( devtools( subscribeWithSelector( persist(() => initialState, { name: "cat-store", partialize: (state) => Object.fromEntries( Object.entries(state).filter(([key]) => !["color"].includes(key)) ), // 部分化,只保存部分数据,排除不必要的内容 }) ) ) ) ); // 通过外部方法增加猫的数量,方法和数据的管理分离 export const increaseBigCats = () => useCatStore.setState((state) => { state.cats.bigCats++; }); export const increaseSmallCats = () => useCatStore.setState((state) => { state.cats.smallCats++; }); export const summary = () => { const state = useCatStore.getState(); return state.cats.bigCats + state.cats.smallCats; }; ``` 在这个 `catStore` 中,我们将 `cats` 状态和更新方法(`increaseBigCats`、`increaseSmallCats`)提取到外部。这样,当我们需要在不同的组件中访问这些数据或修改数据时,只需要引入这些外部方法,而不需要在每个组件中重复定义这些逻辑。 ------ ### **组件的使用:** 接下来,我们来看组件的部分。在 `CatBoxController` 组件中,原本需要直接通过 `useCatStore` 来获取状态并修改数据。但是,由于我们已经将这些方法抽离到外部,我们只需要直接调用外部方法就可以了。 ```js import { increaseBigCats, increaseSmallCats, useCatStore, } from "@/stores/catStore"; const CatBoxController = () => { // 直接从外部 store 获取状态数据 const bigCats = useCatStore.getState().cats.bigCats; // 非响应式数据更新 return (

CatBox

Big Cats: {bigCats}

Random:{Math.random()}

); }; export default CatBoxController; ``` 在这里,我们直接调用了 `useCatStore.getState()` 来获取 `bigCats` 的当前值,并通过 `increaseBigCats` 和 `increaseSmallCats` 来更新它们。这种方式将组件的显示逻辑和状态更新的逻辑完全解耦,提高了代码的可维护性和可重用性。 ------ ### **进一步的抽离:** 如果你在项目中有多个组件都需要访问 `cats` 相关的状态,那么这种外部抽离的做法将极大地减少冗余代码。例如,我们可以创建一个 `CatBox` 组件,通过外部方法来获取 `cats` 状态,并且使用它们来更新 UI。 ```js import { increaseBigCats, increaseSmallCats, summary, useCatStore, } from "@/stores/catStore"; const CatBox = () => { // 从 store 获取所有的 cats 状态 const { cats: { bigCats, smallCats }, } = useCatStore(); return (

CatBox

Big Cats: {bigCats}

Small Cats: {smallCats}

Summary: {summary()}

Random:{Math.random()}

); }; export default CatBox; ``` 在这个 `CatBox` 组件中,所有的状态访问和更新方法都来自于外部的 `catStore`。这样做的好处是,当我们需要修改某个状态的逻辑时,只需要修改 `catStore`,而不需要在每个使用状态的组件中进行修改,从而提升了代码的可维护性。 ------ ### **总结:** 1. **外部抽离数据与方法的好处**:通过将状态和方法从组件中抽离出来,我们能够解耦状态管理和视图渲染,使得代码更加清晰且易于维护。 2. **提高可维护性**:当我们在多个组件中使用相同的状态和方法时,外部抽离可以避免重复代码,并且使得修改状态更新逻辑时只需修改一处,而不需要在每个组件中都做同样的修改。 3. **如何实现**:通过状态管理库(如 Zustand),我们可以将状态和更新方法放在外部 store 中,并在需要的地方进行引用。这样,组件只需要关注数据的展示,业务逻辑则集中在外部 store 中。