强大的 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'
其他代码保持不变

查看效果

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

相关推荐
PleaSure乐事1 分钟前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶2 分钟前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json
getaxiosluo3 分钟前
react jsx基本语法,脚手架,父子传参,refs等详解
前端·vue.js·react.js·前端框架·hook·jsx
理想不理想v6 分钟前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫7 分钟前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.12 分钟前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家1 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds2 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js