强大的 zustand 从使用到源码

前言: 大家好我是小瑜,上周五组会,组长安利了一款React强大的状态管理工具:zustand,并且正好看到光哥的手册中有对应的文章,帮助进行了一波研究。相比于Redux,没有复杂的reducer、useSelector、useDispatch等这些概念。类似于Vue的pinia,使用起来非常简便,特别是源码部分非常巧妙,不到100行就可以实现。接下来用简洁的例子来说明zustand的使用、持久化、如何配合immer以及源码的实现。

1. 基本使用

tsx 复制代码
import React, { useState } from 'react'
import { create } from 'zustand'

const useMyStore = create((set) => ({
  aaa: '',
  bbb: '',
  updateAaa: (value) => set(() => ({ aaa: value })),
  updateBbb: (value) => set(() => ({ bbb: value })),
}))

const Ccc = () => {
  const { aaa } = useMyStore()
  return <p>hello, {aaa}</p>
}

const Bbb = () => {
  return (
    <div>
      <Ccc />
    </div>
  )
}

const App = () => {
  const { aaa, updateAaa } = useMyStore()

  return (
    <div>
      <input onChange={(e) => updateAaa(e.currentTarget.value)} value={aaa} />
      <Bbb />
    </div>
  )
}
export default App

2. subscribe监视器

作用类似于useEffect,会监听到最新的值,页面的值和打印的值同步

tsx 复制代码
/**
 * 监视器
 * 回调函数可以拿到当前 state,或者调用 store.getState 也可以拿到 state
 */
useMyStore.subscribe((state) => {
  console.log(useMyStore.getState())
  if (useMyStore.getState().aaa === 'aaa') {
    return setBool(true)
  }
  return setBool(false)
})

3. 中间件的原理

create 方法的参数,它是一个接受 set、get、store 的三个参数的函数,利用这个特点来前置工作,也就是类似写中间件

这个就是中间件,和redux的中间件是一样的设计。 它并不是zustand本身做啥支持,只要把create的参数设计成一个函数,这个函数接收set、get 等作为参数,那就致残支持了中间件。

注意 此时打印获取的get(),是上一次变化的值,并不是最新的值!

tsx 复制代码
const logMiddleWare = (callback) => {
  return (set, get, store) => {
    function newSet(...args) {
      console.log('调用了 set, 新的state:', get())
      console.log('调用了store', store)
      return set(...args)
    }
    return callback(newSet, get, store)
  }
}

const useMyStore = create(
  logMiddleWare((set) => ({
    aaa: '',
    bbb: '',
    updateAaa: (value) => set(() => ({ aaa: value })),
    updateBbb: (value) => set(() => ({ bbb: value })),
  }))
)

zustand内置了一些中间件,比如immer、persist。

4. persist 持久化存储

打印和页面显示一致

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

const useMyStore = create(
  // persist 就是同步 store 数据到 localStorage 的
  persist(
    (set) => ({
      aaa: '',
      bbb: '',
      updateAaa: (value) => set(() => ({ aaa: value })),
      updateBbb: (value) => set(() => ({ bbb: value })),
    }),
    {
      name: 'wty',
    }
  )
)

5. 自定义中间件可以和persist结合起来

tsx 复制代码
const logMiddleWare = (callback) => {
  return (set, get, store) => {
    function newSet(...args) {
      console.log('调用了 set, 新的state:', get())
      // console.log('调用了store', store)
      return set(...args)
    }
    return callback(newSet, get, store)
  }
}

const useMyStore = create(
  // persist 就是同步 store 数据到 localStorage 的
  logMiddleWare(
    persist(
      (set) => ({
        aaa: '',
        bbb: '',
        updateAaa: (value) => set(() => ({ aaa: value })),
        updateBbb: (value) => set(() => ({ bbb: value })),
      }),
      {
        name: 'wty',
      }
    )
  )
)

6. 配合immer插件

tsx 复制代码
import { create } from 'zustand'
import { produce } from 'immer'

// 创建状态管理器
const useStore = create((set) => ({
  obj: {
    name: 'zs',
    house: {
      city: 'beijing',
      area: 100,
    },
  },
  changeHouse: (name) =>
    set(
      produce((state) => {
        console.log(state)
        state.obj.house.city = name
      })
    ),
  addArea: () =>
    set(
      produce((state) => {
        state.obj.house.area++
      })
    ),
}))

useStore.subscribe((state) => {
  console.log(useStore.getState())
})

function TodoList() {
  const { obj, changeHouse, addArea } = useStore()
  return (
    <div>
      {JSON.stringify(obj)}
      <button onClick={() => changeHouse('大房子')}>Add Todo</button>
      <button onClick={() => addArea()}>Add Area</button>
    </div>
  )
}

export default TodoList

7. 自己手写zushand

tsx 复制代码
import { useEffect, useState, useSyncExternalStore } from 'react'

const createStore = (createState) => {
  // 全局状态容器
  let state
  // 侦听器
  const listeners = new Set()

  /**
   * 核心是判断输入的新值和旧值,将事件并且添加到监听器中并执行,将最新的值添加到状态容器中管理
   * @param {*} partial 就是 () => ({ aaa: value } 或者 { aaa: value })
   * @param {*} replace 
   */
  const setState = (partial, replace) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial
    if (!Object.is(nextState, state)) {
      const previousState = state
      console.log(previousState, 'previousState')
      if (!replace) {
        state =
          typeof nextState !== 'object' || nextState === null
            ? nextState
            : Object.assign({}, state, nextState)
      } else {
        state = nextState
      }
      // 核心在这里,主要修改了数据,会触发侦听器
      listeners.forEach((listener) => listener(state, previousState))
      console.log(listeners, 'listeners')
    }
  }

  // 获取全局状态
  const getState = () => state

  // 每次更新都会触发此函数,新增很删除就会触发新值的更新
  const subscribe = (listener) => {
    listeners.add(listener)
    return () => listeners.delete(listener)
  }

  // 清空订阅
  const destroy = () => {
    listeners.clear()
  }

  const api = { setState, getState, subscribe, destroy }

  // 返回zushand对应的三个参数 分别是 set get store
  state = createState(setState, getState, api)

  return api
}

/**
 * 触发渲染
 * 判断listeners是否一致,如果不一致则进行重新渲染,然后给状态添加一个随机数
 * @param {*} api
 * @param {*} selector
 * @returns
 */
function useStore(api, selector) {
  // 方法二:利用 useSyncExternalStore 触发渲染
  function getState() {
    return selector(api.getState())
  }
  return useSyncExternalStore(api.subscribe, getState)

  // 方法一:更新状态触发渲染
  // const [, forceRender] = useState(0)
  // useEffect(() => {
  //   console.log(api.subscribe, 'subscribe')
  //   api.subscribe((state, prevState) => {
  //     console.log({ state, prevState }, 'state, prevState')
  //     const newObj = selector(state)
  //     const oldobj = selector(prevState)

  //     if (newObj !== oldobj) {
  //       forceRender(Math.random())
  //     }
  //   })
  // }, [])
  // return selector(api.getState())
}

// 入口函数
export const create = (createState) => {
  const api = createStore(createState)

  const useBoundStore = (selector) => useStore(api, selector)

  Object.assign(useBoundStore, api)

  return useBoundStore
}

引入手写的方法

tsx 复制代码
import { create } from './myZustand'
其他代码保持不变

查看效果

完结撒花 ✿✿ヽ(°▽°)ノ✿ ~~~~~

相关推荐
Larcher26 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐38 分钟前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭1 小时前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信1 小时前
我们需要了解的Web Workers
前端
brzhang1 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu2 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花2 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋2 小时前
场景模拟:基础路由配置
前端
六月的可乐2 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程