首页/文章/javascript

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

2024-05-20
20357 分钟
...

说起 Zustand,很多使用 React 的小伙伴肯定不陌生
它是一个 React 状态管理库,类似于 Redux、Mobx
一起来看下 Zustand 的周下载量,大概在 250w,妥妥的第一梯队

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

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

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

三个状态管理库,三种不同的思想,不得不说一句:真牛!!!,有兴趣的同学可以访问以下链接查看:
作者 Github: github.com/dai-shi

看下官网提供的例子:

导入 React 项目中:

import { create } from 'zustand';

type Store = {
  count: number;
  inc: () => void;
};

const useStore = create<Store>(set => ({
  count: 1,
  inc: () => set(state => ({ count: state.count + 1 })),
}));

export default useStore;
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 还支持如下写法:

import { create } from 'zustand';

type Store = {
  count: number;
};


export const useStore = create<Store>(set => ({
  count: 1,
}));


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


export const getStore = () => {
  return useStore.getState();
}

这意味着用户可以在 React 组件外获取和修改 store 的数据,这在有些情况下是非常有用的。
此外,Zustand 还支持中间价,官方提供了几个常用的中间件,我们以 persist持久话缓存为例,看下使用方式:

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函数,就先从它入手(注意以下代码去掉了类型和一些提示类代码):

const createImpl = (createState) => {
  
  const api = typeof createState === 'function' ? createStore(createState) : createState

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

  
  Object.assign(useBoundStore, api)

  
  return useBoundStore
}

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

api 是个什么东西?
useBoundStore 又是个什么东西?
有点云里雾里的感觉,重点是 函数 createStoreuseStore的执行结果。一个一个看:

createStore函数

首先是 createStore函数:

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函数:

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函数:

const createImpl = (createState) => {
  
  const api = typeof createState === 'function' ? createStore(createState) : createState

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

  
  Object.assign(useBoundStore, api)

  
  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 组件之外修改和获取数据
export const inc = () => {
  useStore.setState(state => ({ count: state.count + 1 }));
};


export const getStore = () => {
  return useStore.getState();
}

本文主要讲解了 Zustand 的使用和源码实现,原来,Zustand 不仅使用起来简单,源码更简单。
希望本文能帮助你更好的使用 Zustand。
我是克鲁,我们下期再见。
彩蛋:下期是不是得重点分析下_useSyncExternalStore_的源码呢?如果你有这个需求,请在评论区留下你的足迹,让我知道有多少人有这个需求。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2026-02-24
navigator.sendBeacon全指南
在前端开发中,埋点系统是必不可少的一环。我们经常需要在用户 关闭页面 、 刷新 或 跳转路由 时,向服务器发送最后一条统计数据(比如用户停留时长、页面跳出率)。 但这看似简单的需求,在实现时却危机四伏:请求发不出去?页面跳转卡顿?今天我们就来聊聊这个问题的终极解决方案 —— 。 一、 痛点与传统方案的挣扎 场景还原 当用户点击关闭按钮时,浏览器会触发生命周期事件( 或 )。如果我们直接使用普通的异步 AJAX ( 或 ) 发送请求,浏览...
学习
javascript2025-11-02
理解浏览器事件系统,从用户点击到事件对象的完整旅程
深入理解浏览器事件系统:从用户点击到事件对象的完整旅程 “当我点击页面按钮时,背后发生了什么?为什么回调函数能收到一个包含丰富信息的event对象?今天,让我们一起揭开浏览器事件系统的神秘面纱。” 一个令人困惑的现象 作为前端开发者,我们每天都在写这样的代码: 这段代码如此熟悉,以至于我们很少停下来思考:​ ​这个 对象到底从哪里来?它为什么能知道点击的精确坐标?为什么能识别是哪个元素被点击了?​ ​ 更神奇的是,当我们手动创建事件时:...
学习
javascript2025-10-01
实现大文件上传全流程详解
在日常开发中,大文件上传是个绕不开的坎——动辄几百 MB 甚至 GB 级的文件,直接上传不仅容易超时,还会让用户体验大打折扣。最近我用 Vue+Express 实现了一套完整的大文件上传方案,支持分片上传、断点续传、秒传和手动中。 一、先看效果:我们要实现什么? 先上核心功能清单,确保大家明确目标,知道我们要解决哪些实际问题: 大文件分片上传 :将文件切成固定大小的小片段分批上传,避免单次请求超时 秒传 :服务器已存在完整文件时,直接返...
学习
javascript2025-09-18
JavaScript 的多线程能力:Worker
如果你写过一些计算量稍大的 JavaScript 代码,比如图像处理、大量数据排序或者复杂的算法,你几乎肯定遇到过浏览器“卡死”的现象。点击页面没反应,动画也停了,就像整个世界都静止了。 这就是主线程被阻塞的典型后果。因为主线程既要负责执行 JavaScript,又要负责渲染页面、响应用户操作,一旦它被繁重的计算任务占满,就无暇顾及其他,用户体验便直线下降。 这个问题的根源,正是“主线程是单线程的”。那么,如何解决呢? 答案很简单:把这...
学习面试
javascript2025-09-15
一张 8K 海报差点把首屏拖垮
你给后台管理系统加了一个「企业风采」模块,运营同学一口气上传了 200 张 8K 宣传海报。首屏直接飙到 8.3 s,LCP 红得发紫。 老板一句「能不能像朋友圈那样滑到哪看到哪?」——于是你把懒加载重新翻出来折腾了一轮。 解决方案:三条技术路线,你全踩了一遍 1\. 最偷懒:原生 一行代码就能跑,浏览器帮你搞定。 🔍 关键决策点 2020 年后现代浏览器全覆盖,IE 全军覆没。 必须写死 ,否则 CLS 会抖成 PPT。 适用场景...
学习
javascript2025-09-10
🚀 Web Worker让你的应用丝滑
🌟 引言 在日常的前端开发中,你是否遇到过这样的困扰: 大数据处理时页面卡死 :处理几万条数据时,页面直接卡成PPT,用户点击毫无反应 复杂计算阻塞UI :图片处理、数据分析等计算密集型任务让整个应用假死 文件上传/下载卡顿 :大文件操作时,其他功能完全无法使用 实时数据处理性能差 :WebSocket接收大量数据时,页面渲染严重滞后 今天分享6个Web Worker的核心技巧,让你的应用告别卡顿,用户体验丝滑如德芙! 💡 核心技巧...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录