Zustand不仅使用起来简单,源码更简单

前言

说起 Zustand,很多使用 React 的小伙伴肯定不陌生

它是一个 React 状态管理库,类似于 Redux、Mobx

一起来看下 Zustand 的周下载量,大概在 250w,妥妥的第一梯队

再来看下 Zustand 的 github 标星,截止目前(2024-3-27)已经有 41.6k,妥妥的高赞仓库

如此优秀的 React 状态管理库不得不研究下。

顺便说一下,Zustand 的作者写了三个状态管理库,凭借一己之力,搅混 React 状态管理库的水。就是下面这哥们:

三个状态管理库,三种不同的思想,不得不说一句:真牛!!!,有兴趣的同学可以访问以下链接查看:

作者 Github: github.com/dai-shi

基本使用

看下官网提供的例子:

导入 React 项目中:

javascript 复制代码
import { create } from 'zustand';

type Store = {
  count: number;
  inc: () => void;
};
// 定义store及方法
const useStore = create<Store>(set => ({
  count: 1,
  inc: () => set(state => ({ count: state.count + 1 })),
}));

export default useStore;
javascript 复制代码
import './App.css';
import useStore from './store';

function App() {
  // 在组件中使用
  const { count, inc } = useStore();

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={inc}>one up</button>
    </div>
  );
}

export default App;

查看下运行结果:

如果你不喜欢将方法和数据定义在一个对象中,zustand 还支持如下写法:

javascript 复制代码
import { create } from 'zustand';

type Store = {
  count: number;
};

// store 中自定义数据
export const useStore = create<Store>(set => ({
  count: 1,
}));

// 在store外定义修改函数
export const inc = () => {
  useStore.setState(state => ({ count: state.count + 1 }));
};

// 在store外部获取store内容
export const getStore = () => {
  return useStore.getState();
}

这意味着用户可以在 React 组件外获取和修改 store 的数据,这在有些情况下是非常有用的。

此外,Zustand 还支持中间价,官方提供了几个常用的中间件,我们以 persist持久话缓存为例,看下使用方式:

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

type Store = {
  count: number;
};

export const useStore = create(
  persist<Store>(
    () => ({
      count: 1,
    }),
    {
      name: 'zustand',
    },
  ),
);

export const inc = () => {
  useStore.setState(state => ({ count: state.count + 1 }));
};

看下效果:

以上就是 Zustand 的全部使用方法,是不是很简单,而这就是他迅速占领 React 状态管理第一梯队的原因;

Zustand 使用起来这么简单,那它的源码一定不简单吧!

非也,让我们一起来解开它神秘的面纱。

深入源码

create函数

首先创建 store 会先调用 Zustand 的 create函数,就先从它入手(注意以下代码去掉了类型和一些提示类代码):

javascript 复制代码
const createImpl = (createState) => {
  // 获取api
  const api = typeof createState === 'function' ? createStore(createState) : createState

  // 获取useBoundStore
  const useBoundStore = (selector, equalityFn) => useStore(api, selector, equalityFn)

  // 将useBoundStore于api合并
  Object.assign(useBoundStore, api)

  // 返回 useBoundStore
  return useBoundStore
}

export const create = (createState) => createImpl(createState);

api 是个什么东西?

useBoundStore 又是个什么东西?

有点云里雾里的感觉,重点是 函数 createStoreuseStore的执行结果。一个一个看:

createStore函数

首先是 createStore函数:

javascript 复制代码
const createStoreImpl = createState => {
  let state;
  const listeners = new Set();

  const setState = (partial, replace) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial;
    if (!Object.is(nextState, state)) {
      const previousState = state;
      state =
        replace ?? (typeof nextState !== 'object' || nextState === null)
          ? nextState
          : Object.assign({}, state, nextState);
      listeners.forEach(listener => listener(state, previousState));
    }
  };

  const getState = () => state;

  const getInitialState = () => initialState;

  const subscribe = listener => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };

  const destroy = () => {
    listeners.clear();
  };

  const api = { setState, getState, getInitialState, subscribe, destroy };
  
  const initialState = (state = createState(setState, getState, api));
  
  return api;
};

export const createStore = createState => createStoreImpl(createState);

分析下:

  • createStore函数执行其实就是 createStoreImpl执行,最后返回了一个对象 apiapi中保存有 setState, getState, getInitialState, subscribe, destroy等函数
  • setState函数是主要的改变数据的函数,仔细看下。
    • 首先 nextState用于获取变化后的值
    • 之后使用 Object.is()比较新旧 state,如果不相同则将新值与旧值执行替换或是合并操作(这里需要注意下:Zustand 支持设置 replace变量,true表示 直接使用新值替换旧值,false表示将新旧值合并,相同属性进行覆盖,新值中没有的属性进行保留,具体使用说明参见 Zustand状态合并
    • 最后执行 listeners集合中全部的 listener,这个 listener是个啥东西,现在还不清楚,咱们继续往下看
  • getState函数很简单,直接返回 state
  • getInitialState函数也很简单,返回初始的 state
  • subscribe函数 用于添加自定义的监听函数
  • destroy函数用于清除全部的监听函数
  • 最后调用用户初始传入的 createState函数获取初始的 state

useStore函数

下面看下useStore函数:

javascript 复制代码
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector'
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;

const identity = arg => arg;

export function useStore(api, selector = identity, equalityFn) {
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getInitialState,
    selector,
    equalityFn,
  );

  return slice;
}

useStore很简单,就是调用了 useSyncExternalStoreWithSelector函数,这个函数是 use-sync-external-store/shim/with-selector包中的一个函数。

其实useSyncExternalStoreWithSelector就是对 useSyncExternalStore的一个包装,那useSyncExternalStore是个啥东西:

useSyncExternalStore

useSyncExternalStore是 React 18 中提供的一个新的 hook,主要是用于订阅和读取外部的值。
使用方法
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
参数

  • subscribe:一个函数,接收一个单独的 callback 参数并把它订阅到 store 上。当 store 发生改变,它应当调用被提供的 callback。这会导致组件重新渲染。subscribe 函数会返回清除订阅的函数。这也就解释了刚才我们的疑惑:listener是个啥东西?listener其实就是触发 React 重新渲染的函数以及我们自己定义的监听数据变化后做的副作用函数。
  • getSnapshot:一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用 getSnapshot 必须返回同一个值。如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件。
  • **可选 **getServerSnapshot:一个函数,返回 store 中数据的初始快照。它只会在服务端渲染时,以及在客户端进行服务端渲染内容的 hydration 时被用到。快照在服务端与客户端之间必须相同,它通常是从服务端序列化并传到客户端的。如果你忽略此参数,在服务端渲染这个组件会抛出一个错误。

返回值

该 store 的当前快照,可以在你的渲染逻辑中使用。

具体参考 React 文档中 useSyncExternalStore 的介绍

useSyncExternalStoreWithSelector是在useSyncExternalStore的基础上添加了两个参数:

  • selector:一个函数,用于获取 state 中的部分数据,有了这个参数 useSyncExternalStoreWithSelector的返回值就可以根据selector的结果来返回而不是每次都返回整个 store,相对灵活方便
  • equalityFn:数据比较方法,如果不希望使用 Object.is做数据对比,可以提供自己的对比函数

Zustand 也正是通过这个 hook 实现数据变化更新视图的。

回到 create函数

函数 createStoreuseStore的内部原理我们都已经解释清楚,下面回到最开始的 create函数:

javascript 复制代码
const createImpl = (createState) => {
  // 获取api
  const api = typeof createState === 'function' ? createStore(createState) : createState

  // 获取useBoundStore
  const useBoundStore = (selector, equalityFn) => useStore(api, selector, equalityFn)

  // 将useBoundStore于api合并
  Object.assign(useBoundStore, api)

  // 返回 useBoundStore
  return useBoundStore
}

export const create = (createState) => createImpl(createState);

这下让我们再分析下:

  • api 就是存放着setState, getState, getInitialState, subscribe, destroy函数的一个对象
  • useBoundStore是一个函数,它调用之后会根据传入的 selector返回对应的 store 数据,如果没有传入selector,则会返回整个 store
  • Object.assign(useBoundStore, api)useBoundStore函数和api进行合并,目的就是方便用户使用如下方式修改和获取数据,即在 React 组件之外修改和获取数据
javascript 复制代码
export const inc = () => {
  useStore.setState(state => ({ count: state.count + 1 }));
};

// 在store外部获取store内容
export const getStore = () => {
  return useStore.getState();
}

总结

本文主要讲解了 Zustand 的使用和源码实现,原来,Zustand 不仅使用起来简单,源码更简单。

希望本文能帮助你更好的使用 Zustand。

我是克鲁,我们下期再见。
彩蛋:下期是不是得重点分析下 _useSyncExternalStore_的源码呢?如果你有这个需求,请在评论区留下你的足迹,让我知道有多少人有这个需求。

相关推荐
齐 飞14 分钟前
MongoDB笔记02-MongoDB基本常用命令
前端·数据库·笔记·后端·mongodb
巧克力小猫猿30 分钟前
基于ant组件库挑选框组件-封装滚动刷新的分页挑选框
前端·javascript·vue.js
FinGet39 分钟前
那总结下来,react就是落后了
前端·react.js
前端李易安40 分钟前
手写一个axios方法
前端·vue.js·axios
XinZong1 小时前
【VSCode插件推荐】想准时下班,你需要codemoss的帮助,分享AI写代码的愉快体验,附详细安装教程
前端·程序员
trim1 小时前
写了个可以在工作中快速摄取知识的神器,都来体验体验
前端·产品
ErvinHowell1 小时前
文件MD5生成性能大提升!如何实现分片与Worker优化
前端·vue.js·算法
想做白天梦1 小时前
LeetCode :150. 逆波兰表达式求值(含求后缀表达式和中缀转后缀表达式)
java·前端·算法
s甜甜的学习之旅2 小时前
前端js处理list(数组)
开发语言·前端·javascript
小布布的不2 小时前
MyBatis 返回 Map 或 List<Map>时,时间类型数据,默认为LocalDateTime,响应给前端默认含有‘T‘字符
前端·mybatis·springboot