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

查看效果

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

相关推荐
腾讯TNTWeb前端团队4 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰7 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪7 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪7 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy8 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom8 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom9 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom9 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom9 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom9 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试