Skip to main content

状态管理

面试向系列:Hooks · 渲染与性能 · MobX · Fiber · 并发 · 场景题

从数据可变性上,主流两种分类

  1. 数据可变(mutable)
    • mobx
    • 一些利用 proxy API 的库
  2. 数据不可变(immutable)
    • redux
    • redux 系列变体
    • zustandset 做浅合并更新,写法很轻)

vue中有 数据不可变(immutable)状态管理吗?

答案: 几乎没有;

immutable 适用于函数式编程,vue可以做到函数式,但显然不是最佳实践。

当前主流mutable数据管理 使用技术方案

proxy + reflect

个人角度 不在乎状态管理 是不是 数据不可变

更加简单 高效的 搞定状态管理

核心诉求:

  1. 更少的api
  2. 更少的心智负担

vue: pinia(vuex5.x 版本) 越来越 少的api 和 心智负担

react: zustand / mobx 越来越 少的api 和 心智负担

rxjs只是异步数据流的工具函数,不依赖rxjs 依然可以实现分层治理

世界主流 的状态管理 大多数情况 还是 数据可变的。 数据不可变的状态管理,更加适用于函数式编程。 对于函数式编程,在整体上 并不是主流。 大多数情况是 面向对象+面向过程的组合,很难函数式到底,基本都不够纯粹。


zustand

对应上面的诉求:几乎没有 Provider / boilerplate,store 就是一个 hook。

安装

npm i zustand

创建 store

import { create } from 'zustand'

interface CounterState {
count: number
increase: () => void
decrease: () => void
reset: () => void
}

export const useCounterStore = create<CounterState>()((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 }),
}))

要点:

  • create 返回的是 hook
  • set 会浅合并新状态;也可传入函数 set((state) => nextPartial)
  • action 直接写在 store 里,组件只消费

组件里用(选择器)

function Counter() {
// 只订阅 count:count 变才重渲染
const count = useCounterStore((s) => s.count)
const increase = useCounterStore((s) => s.increase)

return (
<button type="button" onClick={increase}>
count: {count}
</button>
)
}

不要这样写(整 store 一变就重渲染):

const store = useCounterStore()

常用写法:get、异步 action

import { create } from 'zustand'

interface UserState {
user: { id: string; name: string } | null
loading: boolean
fetchUser: (id: string) => Promise<void>
logout: () => void
}

export const useUserStore = create<UserState>()((set, get) => ({
user: null,
loading: false,
fetchUser: async (id) => {
set({ loading: true })
const res = await fetch(`/api/users/${id}`)
const user = await res.json()
// get() 读当前快照,不必依赖闭包里的旧值
console.log('before:', get().user)
set({ user, loading: false })
},
logout: () => set({ user: null }),
}))

任意组件都能调同一份 store,天然跨组件共享,不需要 Context 包一层。

function Profile() {
const user = useUserStore((s) => s.user)
const fetchUser = useUserStore((s) => s.fetchUser)

return (
<button type="button" onClick={() => fetchUser('1')}>
{user?.name ?? '加载用户'}
</button>
)
}

一次取多个字段:useShallow

选择器如果返回 新对象 / 新数组,默认每次引用都不同,容易多余渲染。用 useShallow 做浅比较:

import { useShallow } from 'zustand/react/shallow'

function Panel() {
const { count, increase } = useCounterStore(
useShallow((s) => ({ count: s.count, increase: s.increase })),
)

// 或数组形式
// const [count, increase] = useCounterStore(
// useShallow((s) => [s.count, s.increase] as const),
// )

return (
<button type="button" onClick={increase}>
{count}
</button>
)
}

可选:persist(本地持久化)

需要记住登录态、主题等时,套一层 middleware 即可:

import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'

interface ThemeState {
theme: 'light' | 'dark'
toggle: () => void
}

export const useThemeStore = create<ThemeState>()(
persist(
(set, get) => ({
theme: 'light',
toggle: () =>
set({ theme: get().theme === 'light' ? 'dark' : 'light' }),
}),
{
name: 'theme-storage', // localStorage key,需唯一
// storage: createJSONStorage(() => sessionStorage), // 默认 localStorage
// partialize: (s) => ({ theme: s.theme }), // 只持久化部分字段
},
),
)

小结:日常业务状态优先 zustand —— API 少、无 Provider、选择器控制渲染面;需要响应式可变模型再看 mobx。