普通视图

发现新文章,点击刷新页面。
今天 — 2025年6月9日首页

Zustand 第三章(状态简化)

作者 小满zs
2025年6月9日 09:16

状态简化

回忆一下我们在使用zustand时,是这样引入状态的(如下),通过解构的方式引入状态,但是这样引入会引发一个问题,例如A组件用到了 hobby.basketball 状态,而B组件 没有用到 hobby.basketball 状态,但是更新hobby.basketball这个状态的时候,A组件和B组件都会重新渲染,这样就导致了不必要的重渲染,因为B组件并没有用到hobby.basketball这个状态。

const { name, age, hobby, setHobbyRap, setHobbyBasketball } = useUserStore()
return (
    <div className="left">
        <h1>A组件</h1>
        <div>
            <h3>{name}</h3>
            <div>年龄:<span>{age}</span></div>
            <div>爱好1:<span>{hobby.sing}</span></div>
            <div>爱好2:<span>{hobby.dance}</span></div>
            <div>爱好3:<span>{hobby.rap}</span></div>
            <div>爱好4:<span>{hobby.basketball}</span></div>
            <button onClick={() => setHobbyRap('只因你太美')}>改变爱好rap</button>
            <button onClick={() => setHobbyBasketball('篮球')}>改变爱好basketball</button>
        </div>
    </div>
)

image.png

状态选择器

所以为了规避这个问题,我们可以使用状态选择器,状态选择器可以让我们只选择我们需要的部分状态,这样就不会引发不必要的重渲染。

const name = useUserStore((state) => state.name)
const age = useUserStore((state) => state.age)
const rap = useUserStore((state) => state.hobby.rap)
const basketball = useUserStore((state) => state.hobby.basketball)

image.png

useShallow

你以为这样就结束了? 并没有,你可以想一下如果一个属性很多,例如100个,那我们写起来岂不是要疯了,但是你用解构的话他又会造成不必要的重渲染,真是生与死轮回不止,这时候我们就可以使用useShallow来避免这个问题。

useShallow 只检查顶层对象的引用是否变化,如果顶层对象的引用没有变化(即使其内部属性或子对象发生了变化,但这些变化不影响顶层对象的引用),使用 useShallow 的组件将不会重新渲染

import { useShallow } from 'zustand/react/shallow';
const { rap, name } = useUserStore(useShallow((state) => ({
    rap: state.hobby.rap,
    name: state.name
})))

image.png

代码获取

store/user.ts

import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
interface User {
    name: string,
    age: number,
    hobby: {
        sing: string,
        dance: string,
        rap: string,
        basketball: string,
    }
    setHobbyRap: (rap: string) => void,
    setHobbyBasketball: (basketball: string) => void
}
const useUserStore = create<User>()(immer((set) => ({
    name: '坤坤',
    age: 18,
    hobby: {
        sing: '坤式唱腔',
        dance: '坤式舞步',
        rap: '坤式rap',
        basketball: '坤式篮球'
    },
    setHobbyRap: (rap: string) =>set((state) => {
        state.hobby.rap = rap
    }),
    setHobbyBasketball: (basketball: string) => set((state) => {
        state.hobby.basketball = basketball
    })
})))

export default useUserStore;

A component

import '../index.css'
import useUserStore from '../../store/user';
export default function Left() {
    console.log('A组件渲染')
    const { name, age, hobby, setHobbyRap, setHobbyBasketball } = useUserStore()
    return (
        <div className="left">
            <h1>A组件</h1>
            <div>
                <h3>{name}</h3>
                <div>年龄:<span>{age}</span></div>
                <div>爱好1:<span>{hobby.sing}</span></div>
                <div>爱好2:<span>{hobby.dance}</span></div>
                <div>爱好3:<span>{hobby.rap}</span></div>
                <div>爱好4:<span>{hobby.basketball}</span></div>
                <button onClick={() => setHobbyRap('只因你太美')}>改变爱好rap</button>
                <button onClick={() => setHobbyBasketball('篮球')}>改变爱好basketball</button>
            </div>
        </div>
    )
}

B component

import '../index.css'
import useUserStore from '../../store/user';
import { useShallow } from 'zustand/react/shallow';
export default function Right() {
    console.log('B组件渲染')
    const { rap, name } = useUserStore(useShallow((state) => ({
        rap: state.hobby.rap,
        name: state.name
    })))
    return (
        <div className="right">
            <h1>B组件</h1>
            <div>
                <div>姓名:<span>{name}</span></div>
                <div>rap:<span>{rap}</span></div>
            </div>
        </div>
    )
}

css

.left {
    width: 50%;
    height: 100%;
    border: 1px solid rgb(19, 204, 148);
    height: 300px;
    margin:30px;
    padding: 20px;
    border-radius: 10px;
}


.right {
    width: 50%;
    height: 100%;
    border: 1px solid rgb(214, 35, 35);
    height: 300px;
    margin:30px;
    padding: 20px;
    border-radius: 10px;
}


.left button {
    margin: 10px;
    padding: 10px;
}

.right button {
    margin: 10px;
    padding: 10px;
}

昨天 — 2025年6月8日首页

Zustand 第二章(状态处理)

作者 小满zs
2025年6月7日 22:38

状态处理

在上一章我们讲了,Zustand会合并第一层的state,但是如果深层次应该如何处理呢

来吧演示

首先创建一个葫芦娃,葫芦娃有七个娃,每个娃都有自己的状态,我们可以通过updateGourd来更新葫芦娃的状态,这样就实现了一个深层次的demo

import { create } from 'zustand'

interface User {
    gourd: {
        oneChild: string,
        twoChild: string,
        threeChild: string,
        fourChild: string,
        fiveChild: string,
        sixChild: string,
        sevenChild: string,
    },
    updateGourd: () => void
}
const useUserStore = create<User>(((set) => ({
    //创建葫芦娃
    gourd: {
        oneChild: '大娃',
        twoChild: '二娃',
        threeChild: '三娃',
        fourChild: '四娃',
        fiveChild: '五娃',
        sixChild: '六娃',
        sevenChild: '七娃',
    },
    updateGourd: () => set((state) => ({
        gourd: {
            //...state.gourd, 先不进行状态合并  // [!code highlight] 
            oneChild: '大娃-超进化',
        }
    }))
})))

export default useUserStore;

我们会发现如果不进行状态合并,其他的状态是会丢失的,所以深层次的状态处理需要进行状态合并,但是如果代码过多,每次都需要合并状态也挺烦的,所以我们可以通过immer中间件处理这个问题

state.gif

使用immer中间件

安装

npm install immer

原始immer的用法

需要导出produce,然后它的第一个参数是原始值,第二个参数是一个回调函数,回调函数中的参数是draft,也就是原始值的拷贝,然后我们就可以直接修改draft了,最后返回新的值

import { produce } from 'immer'

const data = {
  user: {
    name: '张三',
    age: 18
  }
}

const newData = produce(data, draft => {
  draft.user.age = 20
})

console.log(newData,data) 
//{ user: { name: '张三', age: 20 } } 
//{ user: { name: '张三', age: 18 } }

immerZustand中的使用方法

引入注意是从zustand/middleware/immer引入,而不是immer

import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
  1. 首先从zustand中间件引入immer
  2. 然后注意结构create()(immer())这里是两个括号而不是放在create里面了
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
interface User {
    gourd: {
        oneChild: string,
        twoChild: string,
        threeChild: string,
        fourChild: string,
        fiveChild: string,
        sixChild: string,
        sevenChild: string,
    },
    updateGourd: () => void
}
//注意结构发生了变化!!!
const useUserStore = create<User>()(immer(((set) => ({
    //创建葫芦娃
    gourd: {
        oneChild: '大娃',
        twoChild: '二娃',
        threeChild: '三娃',
        fourChild: '四娃',
        fiveChild: '五娃',
        sixChild: '六娃',
        sevenChild: '七娃',
    },
    updateGourd: () => set((state) => {
        state.gourd.oneChild = '大娃-超进化' //这儿不需要状态合并了需要修改什么值直接改就行了
        state.gourd.twoChild = '二娃-谁来了'
        state.gourd.threeChild = '三娃-我来了'
    })
}))))

export default useUserStore;

immer原理剖析

immer.js 通过 Proxy 代理对象的所有操作,实现不可变数据的更新。当对数据进行修改时,immer 会创建一个被修改对象的副本,并在副本上进行修改,最后返回修改后的新对象,而原始对象保持不变。这种机制确保了数据的不可变性,同时提供了直观的修改方式。

immer 的核心原理基于以下两个概念:

  1. 写时复制 (Copy-on-Write)
  • 无修改时:直接返回原对象
  • 有修改时:创建新对象
  1. 惰性代理 (Lazy Proxy)
  • 按需创建代理
  • 通过 Proxy 拦截操作
  • 延迟代理创建

工作流程

graph TD
A[调用 produce] --> 
B[创建代理 draft] --> 
C[执行 recipe 修改 draft]  --> 
D[是否有修改!] 
D-- 是 --> E[创建新对象...base + ...modified]   
D-- 否 --> F[直接返回 base]

简化实现

type Draft<T> = {
  -readonly [P in keyof T]: T[P];
};

function produce<T>(base: T, recipe: (draft: Draft<T>) => void): T {
  // 用于存储修改过的对象
  const modified: Record<string, any> = {};
  
  const handler = {
    get(target: any, prop: string) {
      // 如果这个对象已经被修改过,返回修改后的对象
      if (prop in modified) {
        return modified[prop];
      }
      
      // 如果访问的是对象,则递归创建代理
      if (typeof target[prop] === 'object' && target[prop] !== null) {
        return new Proxy(target[prop], handler);
      }
      return target[prop];
    },
    set(target: any, prop: string, value: any) {
      // 记录修改
      modified[prop] = value;
      return true;
    }
  };

  // 创建代理对象
  const proxy = new Proxy(base, handler);
  
  // 执行修改函数
  recipe(proxy);
  
  // 如果没有修改,直接返回原对象
  if (Object.keys(modified).length === 0) {
    return base;
  }
  
  // 创建新对象,只复制修改过的属性
  return JSON.parse(JSON.stringify(proxy))
}

// 使用示例
const state = {
  user: {
    name: '张三',
    age: 25
  }
};

const newState = produce(state, draft => {
  draft.user.name = '李四';
  draft.user.age = 26;
});

console.log(state);     // { user: { name: '张三', age: 25 } }
console.log(newState);  // { user: { name: '李四', age: 26 } }

这儿只是简单实现,没有考虑数组的情况和深层次的代理,只实现了其核心思想

❌
❌