zustand 源码解析

news2025/4/18 17:09:56

文章目录

  • 实现原理
  • create
  • createStore 创建实例
  • CreateStoreImpl 实现发布订阅
  • createImpl 包装返回给用户调用的 hook
  • useSyncExternalStoreWithSelector 订阅更新
  • zustand 性能优化
    • 自定义数据更新
      • createWithEqualityFn
      • createWithEqualityFnImpl 返回 hook
      • useSyncExternalStoreWithSelector 自定义比较函数
    • 使用 useShallow 浅比较
      • useShallow
      • shallow 浅比较实现
    • 使用 immer
      • 在组件外部数据订阅
  • SSR 数据同步

实现原理

  • 通过发布订阅管理数据状态,和客户端 useSyncExternalStoreWithSelector 解耦,支持 SSR
  • 通过 useSyncExternalStoreWithSelector 结合上面步骤实现数据更新
  • 通过 useSyncExternalStoreWithSelector 的自定义切片数据、自定义是否更新状态函数、浅比较数据实现性能优化
  • 数据 set 时通过Object.is 比较是否变化
    • 相比 === 的差异在 NAN、±0 的比较上
  • 数据 set 时的合并通过 Object.assign 实现

create

export const create = (<T>(createState: StateCreator<T, [], []> | undefined) =>
  createState ? createImpl(createState) : createImpl) as Create

createStore 创建实例

export const createStore = ((createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore

CreateStoreImpl 实现发布订阅

  • 通过闭包保存 state
  • 通过发布订阅管理 state
  • 数据的合并通过 Object.assign实现
  • 这样实现的好处是可以在服务端使用,而不依赖于 useSyncExternalStoreWithSelector
const createStoreImpl: CreateStoreImpl = (createState) => {
  type TState = ReturnType<typeof createState>
  type Listener = (state: TState, prevState: TState) => void
  
  let state: TState
  // Set 去重
  const listeners: Set<Listener> = new Set()

  const setState: StoreApi<TState>['setState'] = (partial, replace) => {
    // 判断 set 时传入的是对象还是函数
    const nextState =
      typeof partial === 'function'
        ? (partial as (state: TState) => TState)(state)
        : partial
    // 判断数据是否变化
    if (!Object.is(nextState, state)) {
      const previousState = state
      // 是否直接 replace 替换 state,还是 merge 合并 state
      state =
        replace ?? (typeof nextState !== 'object' || nextState === null)
          ? (nextState as TState)
          : Object.assign({}, state, nextState)
      // 数据变换后发布通知
      listeners.forEach((listener) => listener(state, previousState))
    }
  }
  
  // 直接获取保存的闭包 state
  const getState: StoreApi<TState>['getState'] = () => state
  // 添加订阅
  const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
    listeners.add(listener)
    // Unsubscribe
    return () => listeners.delete(listener)
  }
  
  // 取消所有订阅直接调用 Set 的 clear
  const destroy: StoreApi<TState>['destroy'] = () => {
    if (import.meta.env?.MODE !== 'production') {
      console.warn(
        '[DEPRECATED] The `destroy` method will be unsupported in a future version. Instead use unsubscribe function returned by subscribe. Everything will be garbage-collected if store is garbage-collected.',
      )
    }
    listeners.clear()
  }

  const api = { setState, getState, subscribe, destroy }
  // createState 是用户传入的函数,传入的函数会拿到 setState、getState 和 api 方法
  state = createState(setState, getState, api)
  return api as any
}

createImpl 包装返回给用户调用的 hook

  • 实际是先调用上面的 createStore 先为状态创建对应的发布订阅实例
  • 再通过 useStore 将发布订阅和 useSyncExternalStoreWithSelector 联系起来进而可以发布订阅后通知 React 进行更新
const createImpl = <T>(createState: StateCreator<T, [], []>) => {
  if (
    import.meta.env?.MODE !== 'production' &&
    typeof createState !== 'function'
  ) {
    console.warn(
      "[DEPRECATED] Passing a vanilla store will be unsupported in a future version. Instead use `import { useStore } from 'zustand'`.",
    )
  }
  // 管理状态的实例(发布订阅)
  const api =
    typeof createState === 'function' ? createStore(createState) : createState
    
  // 交给用户调用的 hook,通过 useStore 封装 useSyncExternalStoreWithSelector
  const useBoundStore: any = (selector?: any, equalityFn?: any) =>
    useStore(api, selector, equalityFn)

  // 添加额外 API
  Object.assign(useBoundStore, api)

  return useBoundStore
}

useSyncExternalStoreWithSelector 订阅更新

  • useSyncExternalStoreWithSelector 比 useSyncExternalStore 性能更好,可以定义想订阅的状态
// 通过 useSyncExternalStoreWithSelector hook 订阅外部存储,并返回你关心的数据部分
export function useStore<TState, StateSlice>(
  api: WithReact<StoreApi<TState>>,
  selector: (state: TState) => StateSlice = api.getState as any,
  equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) {
  if (
    import.meta.env?.MODE !== 'production' &&
    equalityFn &&
    !didWarnAboutEqualityFn
  ) {
    console.warn(
      "[DEPRECATED] Use `createWithEqualityFn` instead of `create` or use `useStoreWithEqualityFn` instead of `useStore`. They can be imported from 'zustand/traditional'. https://github.com/pmndrs/zustand/discussions/1937",
    )
    didWarnAboutEqualityFn = true
  }
  // 返回状态,此时发布订阅和 React 更新关联上了
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,
    selector, // 选择外部存储中你关心的数据部分
    equalityFn,// 自定义是否重新渲染
  )
  useDebugValue(slice)
  return slice
}

zustand 性能优化

自定义数据更新

  • zustand 支持传入一个自定义比较函数确定数据是否变化
    • 实质是 useSyncExternalStoreWithSelector 支持传入比较函数

createWithEqualityFn

  • 不适用默认的 create ,使用 createWithEqualityFn 并传入比较函数
export const createWithEqualityFn = (<T>(
  createState: StateCreator<T, [], []> | undefined,
  defaultEqualityFn?: <U>(a: U, b: U) => boolean,
) =>
  createState
    ? createWithEqualityFnImpl(createState, defaultEqualityFn)
    : createWithEqualityFnImpl) as CreateWithEqualityFn

createWithEqualityFnImpl 返回 hook

  • 取消 Object.is 比较数据变化
const createWithEqualityFnImpl = <T>(
  createState: StateCreator<T, [], []>,
  defaultEqualityFn?: <U>(a: U, b: U) => boolean,
) => {
  // 创建发布订阅实例
  const api = createStore(createState)
  
  // 传入自定义比较函数,取消默认的 Object.is 比较
  const useBoundStoreWithEqualityFn: any = (
    selector?: any,
    equalityFn = defaultEqualityFn,
  ) => useStoreWithEqualityFn(api, selector, equalityFn)

  Object.assign(useBoundStoreWithEqualityFn, api)
  // 返回用户调用的 hook 
  return useBoundStoreWithEqualityFn
}

useSyncExternalStoreWithSelector 自定义比较函数

export function useStoreWithEqualityFn<
  S extends WithReact<StoreApi<unknown>>,
  U,
>(
  api: S,
  selector: (state: ExtractState<S>) => U,
  equalityFn?: (a: U, b: U) => boolean,
): U

export function useStoreWithEqualityFn<TState, StateSlice>(
  api: WithReact<StoreApi<TState>>,
  selector: (state: TState) => StateSlice = api.getState as any,
  equalityFn?: (a: StateSlice, b: StateSlice) => boolean,
) {
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,
    selector,
    equalityFn,
  )
  useDebugValue(slice)
  return slice
}

使用 useShallow 浅比较

  • 可以通过 useShallow 浅比较提升性能
const { nuts, honey } = useBearStore(
  useShallow((state) => ({ nuts: state.nuts, honey: state.honey })),
)
  • 浅比较的更新策略,可以看到相同属性的对象就算是新对象,比较时都会默认无变化提升性能
  shallow(1, 1); // true
  shallow('hello', 'hello'); // true
  shallow({ a: 1 }, { a: 1 }); // true
  shallow([1, 2, 3], [1, 2, 3]); // false

useShallow

  • 保存 selector 结果,对应的结果会给到 useSyncExternalStoreWithSelector 的第四个参数,如果提供了第五个参数比较函数,则使用比较函数判断两次结果,默认第五个比较函数为 Object.is ,会跳过更新
  const slice = useSyncExternalStoreWithSelector(
    api.subscribe,
    api.getState,
    api.getServerState || api.getState,  
    selector, // 选择外部存储中你关心的数据部分,对应 useShallow 返回的函数
    equalityFn,// 自定义是否重新渲染,默认为Object.is
  )
const { useRef } = ReactExports

export function useShallow<S, U>(selector: (state: S) => U): (state: S) => U {
  const prev = useRef<U>()
  // 通过闭包保存 selector 返回的结果,对应的结果会给到 useSyncExternalStoreWithSelector 的第四个参数 selector,如果返回的引用不变,不会出触发更新
  return (state) => {
    const next = selector(state)
    return shallow(prev.current, next)
      ? (prev.current as U)
      : (prev.current = next)
  }
}

shallow 浅比较实现

  • 最主要的是判断当对象 key-value 一样时,不管是否是新建对象都会是认为一样的
export function shallow<T>(objA: T, objB: T) {
  if (Object.is(objA, objB)) {
    return true
  }
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false
  }

  if (objA instanceof Map && objB instanceof Map) {
    if (objA.size !== objB.size) return false

    for (const [key, value] of objA) {
      if (!Object.is(value, objB.get(key))) {
        return false
      }
    }
    return true
  }

  if (objA instanceof Set && objB instanceof Set) {
    if (objA.size !== objB.size) return false

    for (const value of objA) {
      if (!objB.has(value)) {
        return false
      }
    }
    return true
  }

  const keysA = Object.keys(objA)
  if (keysA.length !== Object.keys(objB).length) {
    return false
  }
  for (let i = 0; i < keysA.length; i++) {
    if (
      !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
      !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
    ) {
      return false
    }
  }
  return true
}

/**
 * 
  shallow(1, 1); // true
  shallow('hello', 'hello'); // true
  shallow({ a: 1 }, { a: 1 }); // true
  shallow([1, 2, 3], [1, 2, 3]); // false
 */

使用 immer

  • immer 的中间件实现主要是拦截了发布订阅的 set 方法,通过immer去修改数据后,再调用原始 set 方法
const useBeeStore = create(
  immer((set) => ({
    bees: 0,
    addBees: (by) =>
      set((state) => {
        state.bees += by
      }),
  })),
)
const immerImpl: ImmerImpl = (initializer) => (set, get, store) => {
  type T = ReturnType<typeof initializer>
  // 拦截了 set 方法 
  store.setState = (updater, replace, ...a) => {
    // 通过 immer 修改数据
    const nextState = (
      typeof updater === 'function' ? produce(updater as any) : updater
    ) as ((s: T) => T) | T | Partial<T>
    // 修改后再调用原始方法
    return set(nextState as any, replace, ...a)
  }

  return initializer(store.setState, get, store)
}

export const immer = immerImpl as unknown as Immer

在组件外部数据订阅

  • 同样是拦截了 subscribe 方法,添加订阅逻辑
const subscribeWithSelectorImpl: SubscribeWithSelectorImpl =
  (fn) => (set, get, api) => {
    type S = ReturnType<typeof fn>
    type Listener = (state: S, previousState: S) => void
    const origSubscribe = api.subscribe as (listener: Listener) => () => void
    // 拦截 subscribe 
    api.subscribe = ((selector: any, optListener: any, options: any) => {
      let listener: Listener = selector // if no selector
      if (optListener) {
        const equalityFn = options?.equalityFn || Object.is
        // TODO:这样后续更新后,无论如何都会更新,因为只比较第一次保存的状态
        let currentSlice = selector(api.getState())
        // 添加本次 listener
        listener = (state) => {
          const nextSlice = selector(state)
          // 只有当数据变化时才触发
          if (!equalityFn(currentSlice, nextSlice)) {
            const previousSlice = currentSlice
            optListener((currentSlice = nextSlice), previousSlice)
          }
        }
        if (options?.fireImmediately) {
          optListener(currentSlice, currentSlice)
        }
      }
      return origSubscribe(listener)
    }) as any
    const initialState = fn(set, get, api)
    return initialState
  }
export const subscribeWithSelector =
  subscribeWithSelectorImpl as unknown as SubscribeWithSelector

SSR 数据同步

  • 服务器返回的数据先存储在 localStroage 里,然后等组件 ready 后再水合 rehydrate 放进 zustand 中

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2331093.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ETPNav:基于演进拓扑规划的连续环境视觉语言导航模型

1、现有工作的缺陷&#xff1a; 最近&#xff0c;出现了一种基于模块化航路点的方法的新兴趋势&#xff0c;该方法将复杂任务分为航路点生成、子目标规划和导航控制&#xff1a; &#xff08;1&#xff09;在每个决策循环中&#xff0c;代理使用预训练的网络来预测附近的几个…

Spring Cloud LoadBalancer负载均衡+算法切换

目录 介绍核心功能负载均衡启动两个支付服务订单模块引入依赖LoadBalanced 注解启动订单服务测试结果 负载均衡算法切换总结 介绍 Spring Cloud LoadBalancer 是 Spring Cloud 提供的客户端负载均衡解决方案&#xff0c;提供更现代化的 API 和更好的 Spring 生态系统集成。它支…

游戏引擎学习第210天

回顾并为今天的工作做准备 今天我们&#xff0c;进行一些编码工作。这部分的编码内容对那些对代码架构感兴趣的人非常有帮助&#xff0c;我认为今天的编码内容会很有教育意义&#xff0c;尤其是在展示一些代码转化的过程中&#xff0c;希望大家能够从中获得一些启发。 接下来…

UML类图综合实验三补档

1.使用简单工厂模式模拟女娲(Nvwa)造人(Person)&#xff0c;如果传入参数“M”&#xff0c;则返回一个Man对象&#xff0c;如果传入参数“W”&#xff0c;则返回一个Woman对象&#xff0c;用Java语言实现该场景。现需要增加一个新的Robot类&#xff0c;如果传入参数“R”&#…

WinForm真入门(11)——ComboBox控件详解

WinForm中 ComboBox 控件详解‌ ComboBox 是 WinForms 中一个集文本框与下拉列表于一体的控件&#xff0c;支持用户从预定义选项中选择或直接输入内容。以下从核心属性、事件、使用场景到高级技巧的全面解析&#xff1a; 一、ComboBox 核心属性‌ 属性说明示例‌Items‌下拉…

DeepSeek底层揭秘——《推理时Scaling方法》技术对比浅析

4月初&#xff0c;DeepSeek 提交到 arXiv 上的最新论文正在 AI 社区逐渐升温。 笔者尝试对比了“关于推理时Scaling”与现有技术&#xff0c;粗浅分析如下&#xff1a; 与LoRA的对比 区别&#xff1a; 应用场景&#xff1a;LoRA是一种参数高效微调方法&#xff0c;主要用于在…

Android Coli 3 ImageView load two suit Bitmap thumb and formal,Kotlin(四)

Android Coli 3 ImageView load two suit Bitmap thumb and formal&#xff0c;Kotlin&#xff08;四&#xff09; 对 Android Coli 3 ImageView load two suit Bitmap thumb and formal&#xff0c;Kotlin&#xff08;三&#xff09;-CSDN博客 进行完善&#xff0c;注意完善 …

Adam优化器研究综述

摘要 Adam优化器&#xff08;Adaptive Moment Estimation&#xff09;是一种广泛应用于深度学习的优化算法&#xff0c;通过自适应学习率加速梯度下降过程。本文从Adam的定义、算法原理、优势与局限性、应用场景及变体等方面进行调研&#xff0c;结合学术文献和实践经验&#x…

在 macOS 上连接 PostgreSQL 数据库(pgAdmin、DBeaver)

在 macOS 上连接 PostgreSQL 数据库 pgAdmin 官方提供的图形化管理工具&#xff0c;支持 macOS。 下载地址&#xff1a;https://www.pgadmin.org/ pgAdmin 4 是对 pgAdmin 的完全重写&#xff0c;使用 Python、ReactJs 和 Javascript 构建。一个用 Electron 编写的桌面运行时…

2018年真题

数学基础 一、 &#xff08;共4分&#xff09;用逻辑符号表达下列语句&#xff08;论域为包含一切事物的集合&#xff09; 1、&#xff08;2分&#xff09;集合A的任一元素的元素都是A的元素 经过对图片文字的识别与逻辑分析&#xff0c;结果如下&#xff1a; 符号定义&…

Efficient Burst Raw Denoising:稳定噪声方差和分频率降噪

Efficient Burst Raw Denoising with Stabilization and Multi-Frequency Denoising Network Burst Raw Denoising必要性Burst Raw Image Denoising流程Main Contributions具体方法介绍集成noise priorCMOS sensor 噪声建模噪声变换&#xff08;Variance stabilization&#xf…

mapbox进阶,使用本地dem数据,加载hillshade山体阴影图层

👨‍⚕️ 主页: gis分享者 👨‍⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍⚕️ 收录于专栏:mapbox 从入门到精通 文章目录 一、🍀前言1.1 ☘️mapboxgl.Map 地图对象1.2 ☘️mapboxgl.Map style属性1.3 ☘️hillshade 山体阴影图层 api1.3.1 ☘️…

【C++】Stack Queue 仿函数

&#x1f4dd;前言&#xff1a; 这篇文章我们来讲讲STL中的stack和queue。因为前面我们已经有了string、vector和list的学习基础&#xff0c;所以这篇文章主要关注一些stack和queue的细节问题&#xff0c;以及了解一下deque&#xff08;缝合怪&#xff09;和priority_queue &am…

代码随想录_单调栈

代码随想录_单调栈 739.每日温度 739. 每日温度 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c;下一个更高温度出现在几天后。如果气温在这之后都不会升高&#xff0c;…

BoostSearch搜索引擎项目 —— 测试用例设计 + web自动化测试代码

web自动化代码&#xff1a; https://gitee.com/chicken-c/boost-search/tree/master/AutoTest

【Ansible自动化运维】一、初步了解,开启自动化运维之旅

在当今数字化时代&#xff0c;随着企业 IT 基础设施规模的不断扩大&#xff0c;传统的手工运维方式逐渐显得力不从心。自动化运维技术应运而生&#xff0c;其中 Ansible 凭借其简洁易用、功能强大的特点&#xff0c;成为众多运维工程师和开发人员的首选工具。本篇文章将从基础概…

条件概率、概率乘法公式、全概率公式和贝叶斯 (Bayes) 公式

定义 设 P ( A ) > 0 P(A) > 0 P(A)>0&#xff0c;若在随机事件 A A A发生的条件下随机事件 B B B发生的概率记作 P ( B ∣ A ) P(B|A) P(B∣A)&#xff0c;定义 P ( B ∣ A ) P ( A B ) P ( A ) P(B|A) \frac{P(AB)}{P(A)} P(B∣A)P(A)P(AB)​ 则称 P ( B ∣ A ) …

kotlin,Android,jetpack compose,日期时间设置

AI生成&#xff0c;调试出来学习&#xff0c;这些小组件会用了&#xff0c;就可以组合一个大点的程序了。 package com.example.mydatetimeimport android.app.AlertDialog import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.co…

ASP.NET图书馆借阅系统(源码+lw+部署文档+讲解),源码可白嫖!

摘要 近些年来&#xff0c;随着科技的飞速发展&#xff0c;互联网的普及逐渐延伸到各行各业中&#xff0c;给人们生活带来了十分的便利&#xff0c;图书馆借阅系统利用计算机网络实现信息化管理&#xff0c;使图书信息、图书借阅、归还的管理发展和服务水平有显著提升。 本文拟…

vi/vim常用快捷键

那么今天我们继续昨天没有介绍完的vi编辑器,来看看常用的一些快捷键,方便我们对文件的编辑. 1.拷贝当前行yy,拷贝当前行向下的5行5yy,并粘贴(输入p) 2.删除当前行dd,删除当前行向下的5行5d 3.在文件中查找某个单词[命令模式/关键字,回车查找,输入n就是查找下一个] ⭐️&…