Zustand 使用优化:深入探讨状态管理性能提升

Zustand 使用优化:深入探讨状态管理性能提升

细粒度订阅:只订阅你需要的状态

在 Zustand 中,默认情况下,当状态发生变化时,所有订阅该 store 的组件都会重新渲染。但在实际场景中,我们可能只需要部分状态的变化触发组件更新。为了解决这个问题,Zustand 提供了细粒度订阅的方式,通过 useStore 钩子实现。
示例代码:

javascript 复制代码
const useStore = createStore({
  count: 0,
  name: 'Alice'
});
function Component() {
  const count = useStore(state => state.count); // 只订阅 count
  const name = useStore(state => state.name);   // 只订阅 name
  return (
    <div>
      <p>Count: {count}</p>
      <p>Name: {name}</p>
    </div>
  );
}

解释:

  • 通过 useStore(state => state.xxx) 的方式,组件只订阅特定的状态部分。
  • 这种方式避免了因其他状态变化导致的不必要的组件重新渲染。
  • 特别适用于需要独立控制不同状态更新的场景。

使用 createWithEqualityFn 优化状态更新

默认情况下,Zustand 的状态更新是通过浅比较(shallow comparison)来判断是否需要重新渲染组件。但在某些复杂场景中,这种比较方式可能不够精确,导致不必要的渲染。这时,我们可以使用 createWithEqualityFn 提供自定义的等式函数。
示例代码:

javascript 复制代码
const useStore = createWithEqualityFn(
  (set) => ({
    items: [],
    addNewItem: (item) => set((state) => ({ items: [...state.items, item] }))
  }),
  (prev, next) => {
    // 自定义等式函数,仅比较 items 数组的变化
    return prev.items === next.items;
  }
);
functionListComponent() {
  const items = useStore(state => state.items);
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

解释:

  • createWithEqualityFn 的第二个参数是一个自定义的等式函数,用于判断状态是否发生变化。
  • 在此示例中,只有当 items 数组发生变化时,组件才会重新渲染。
  • 这种方式适用于需要精确控制状态更新的场景,避免因浅比较导致的误判。

createWithEqualityFn + 细粒度订阅优化

未优化的代码
jsx 复制代码
import create from 'zustand'

const useStore = create(set => ({
  count: 0,
  inc: () => set(state => ({ count: state.count + 1 }))
}))

使用时

jsx 复制代码
const Counter = () => {
  const { count, inc } = useStore()
  return (
    <div>
      <p>{count}</p>
      <button onClick={inc}>+</button>
    </div>
  )
}

上述代码中,count 改变后,Counter 会重渲染,但如果使用了更多字段,任意一个字段变化都会触发整组件渲染 ------ 性能隐患。

进行优化
jsx 复制代码
import { createWithEqualityFn } from 'zustand/traditional'

const useStore = createWithEqualityFn(set => ({
  count: 0,
  name: 'zustand',
  inc: () => set(state => ({ count: state.count + 1 }))
}))

使用时只监听 count

tsx 复制代码
const Counter = () => {
  const count = useStore(state => state.count)
  return <p>{count}</p>
}

这样做可以确保组件只在 count 变化时才更新,不依赖对象整体变更。

细粒度订阅原理与优化方式

Zustand 提供了灵活的 selector 支持,我们可以通过传入函数来选择性订阅状态的某一部分,避免组件对整个状态对象订阅,提高性能。

使用 selector + equalityFn 避免不必要更新

tsx 复制代码
const useStore = createWithEqualityFn((set) => ({
  user: { name: 'John', age: 30 },
  updateName: (name: string) => set(state => ({ user: { ...state.user, name } }))
}))
tsx 复制代码
const UserName = () => {
  const name = useStore(state => state.user.name)
  return <div>{name}</div>
}

如果没有 createWithEqualityFn,哪怕只更新了 age,也会导致 name 组件重渲染。


使用 Immer 管理不可变状态

Zustand 的状态管理基于不可变更新的原则。为了简化不可变状态的管理,我们可以结合 Immer 库。Immer 提供了一个 produce 方法,使得不可变更新变得简单直观。
示例代码:

javascript 复制代码
import produce from 'immer';
const useStore = createStore((set) => ({
  user: {
    name: 'Alice',
    age: 25
  },
  updateUser: (updates) =>
    set((state) =>
      produce(state, (draft) => {
        Object.assign(draft.user, updates);
      })
    )
}));
function Profile() {
  const user = useStore(state => state.user);
  const updateUser = useStore(state => state.updateUser);
  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
      <button
        onClick={() => updateUser({ age: 26 })}
        style={{ cursor: 'pointer' }}
      >
        Increment Age
      </button>
    </div>
  );
}

解释:

  • 使用 Immer 的 produce 方法,我们可以直接在草案(draft)上进行修改,而无需手动创建新对象。
  • 这种方式简化了不可变更新的实现,同时避免了手动操作带来的潜在错误。
  • 特别适合处理嵌套较深或复杂的对象结构。

使用 subscribeWithSelector 实现精准订阅

除了 useStore 钩子,Zustand 还提供了 subscribeWithSelector 方法,允许我们在订阅时使用选择器(selector),从而进一步优化状态更新。

subscribeWithSelector 中间件支持精确地监听 store 中某一部分的变化,适用于非组件环境中,如:日志收集、外部状态联动等。
示例代码:

tsx 复制代码
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'

const useStore = create(
  subscribeWithSelector((set) => ({
    theme: 'light',
    toggleTheme: () => set(state => ({ theme: state.theme === 'light' ? 'dark' : 'light' }))
  }))
)

监听某一状态

tsx 复制代码
useEffect(() => {
  const unsub = useStore.subscribe(
    state => state.theme,
    (theme, prev) => {
      console.log(`Theme changed from ${prev} to ${theme}`)
    }
  )
  return unsub
}, [])

该方式不会触发组件渲染,适用于全局监听场景。

最佳实践

多 store 模式下的优化建议

在大型项目中推荐将不同逻辑拆分为多个 store,减少无关组件的耦合。

tsx 复制代码
// useUserStore.ts
export const useUserStore = createWithEqualityFn(set => ({
  user: null,
  setUser: (user) => set({ user })
}))

// useThemeStore.ts
export const useThemeStore = createWithEqualityFn(set => ({
  theme: 'light',
  toggle: () => set(state => ({ theme: state.theme === 'light' ? 'dark' : 'light' }))
}))

这样就可以做到组件级别状态订阅颗粒度精细,同时每个 store 的逻辑独立、便于维护与测试。

封装 hooks 与 selector 提升复用性

在大型项目中推荐封装状态 hooks,避免散落式使用 useStore(state => state.xxx),便于维护、切换 store 结构时更少代码改动。

tsx 复制代码
// store/user.ts
import { createWithEqualityFn } from 'zustand/traditional'

export const useUserStore = createWithEqualityFn((set) => ({
  user: { name: '', age: 0 },
  setName: (name: string) => set((state) => ({
    user: { ...state.user, name }
  }))
}))
tsx 复制代码
// hooks/useUserName.ts
import { useUserStore } from '@/store/user'

export const useUserName = () =>
  useUserStore((state) => state.user.name)
tsx 复制代码
// pages/Profile.tsx

import React from 'react'
import { useUserName } from '@/hooks/useUserName'

const Profile = () => {
  const name = useUserName()

  return <div>User: {name}</div>
}

export default Profile

可在后续替换 store 实现时仅修改 hooks 层即可

相关推荐
henujolly37 分钟前
网络资源缓存
前端
yuren_xia4 小时前
Spring Boot中保存前端上传的图片
前端·spring boot·后端
普通网友5 小时前
Web前端常用面试题,九年程序人生 工作总结,Web开发必看
前端·程序人生·职场和发展
站在风口的猪11086 小时前
《前端面试题:CSS对浏览器兼容性》
前端·css·html·css3·html5
青莳吖8 小时前
使用 SseEmitter 实现 Spring Boot 后端的流式传输和前端的数据接收
前端·spring boot·后端
CodeCraft Studio8 小时前
PDF处理控件Aspose.PDF教程:在 C# 中更改 PDF 页面大小
前端·pdf·c#
拉不动的猪8 小时前
TS常规面试题1
前端·javascript·面试
再学一点就睡9 小时前
实用为王!前端日常工具清单(调试 / 开发 / 协作工具全梳理)
前端·资讯·如何当个好爸爸
穗余9 小时前
NodeJS全栈开发面试题讲解——P5前端能力(React/Vue + API调用)
javascript·vue.js·react.js
Jadon_z9 小时前
vue2 项目中 npm run dev 运行98% after emitting CopyPlugin 卡死
前端·npm