zustand 源码解析

文章目录

  • 实现原理
  • 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) => voidlet 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 合并 statestate =replace ?? (typeof nextState !== 'object' || nextState === null)? (nextState as TState): Object.assign({}, state, nextState)// 数据变换后发布通知listeners.forEach((listener) => listener(state, previousState))}}// 直接获取保存的闭包 stateconst getState: StoreApi<TState>['getState'] = () => state// 添加订阅const subscribe: StoreApi<TState>['subscribe'] = (listener) => {listeners.add(listener)// Unsubscribereturn () => listeners.delete(listener)}// 取消所有订阅直接调用 Set 的 clearconst 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 封装 useSyncExternalStoreWithSelectorconst useBoundStore: any = (selector?: any, equalityFn?: any) =>useStore(api, selector, equalityFn)// 添加额外 APIObject.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,
): Uexport 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); // trueshallow('hello', 'hello'); // trueshallow({ a: 1 }, { a: 1 }); // trueshallow([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 } = ReactExportsexport 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 falsefor (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 falsefor (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); // trueshallow('hello', 'hello'); // trueshallow({ a: 1 }, { a: 1 }); // trueshallow([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) => voidconst origSubscribe = api.subscribe as (listener: Listener) => () => void// 拦截 subscribe api.subscribe = ((selector: any, optListener: any, options: any) => {let listener: Listener = selector // if no selectorif (optListener) {const equalityFn = options?.equalityFn || Object.is// TODO:这样后续更新后,无论如何都会更新,因为只比较第一次保存的状态let currentSlice = selector(api.getState())// 添加本次 listenerlistener = (state) => {const nextSlice = selector(state)// 只有当数据变化时才触发if (!equalityFn(currentSlice, nextSlice)) {const previousSlice = currentSliceoptListener((currentSlice = nextSlice), previousSlice)}}if (options?.fireImmediately) {optListener(currentSlice, currentSlice)}}return origSubscribe(listener)}) as anyconst initialState = fn(set, get, api)return initialState}
export const subscribeWithSelector =subscribeWithSelectorImpl as unknown as SubscribeWithSelector

SSR 数据同步

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

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

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

相关文章

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…

构建k8s下Helm私有仓库与自定义Chart开发指南

#作者&#xff1a;程宏斌 文章目录 自定义helm模板1、开发自己的chare包2、调试chart3、安装chart 自定义helm模板 https://hub.helm.sh/ 1、开发自己的chare包 [rootmaster ~]# helm create mychare //创建一个名为mychare的chare包 [rootmaster ~]# tree -C mychare/ //以…

MOP数据库中的EXPLAIN用法

EXPLAIN 是 SQL 中的一个非常有用的工具&#xff0c;主要用于分析查询语句的执行计划。执行计划能展示数据库在执行查询时的具体操作步骤&#xff0c;像表的读取顺序、使用的索引情况、数据的访问方式等&#xff0c;这有助于我们对查询性能进行优化。 语法 不同的数据库系统&…

项目范围蔓延的十大诱因及应对策略

项目范围蔓延的十大诱因及应对策略是什么&#xff1f;主要在于&#xff1a; 缺乏清晰目标、利益相关方过多、需求变更未及时管控、缺少优先级体系、沟通链条冗长、管理层干预频繁、资源与预算不匹配、技术风险被低估、合同或协议不完善、缺乏阶段性验收与复盘。其中缺乏清晰目标…

做好一个测试开发工程师第二阶段:java入门:idea新建一个project后默认生成的.idea/src/out文件文件夹代表什么意思?

时间&#xff1a;2025.4.8 一、前言 关于Java与idea工具安装不再展开&#xff0c;网上很多教程&#xff0c;可以自己去看 二、project建立后默认各文件夹代表意思 1、首先new---->project后会得到文件如图 其中&#xff1a; .idea文件代表&#xff1a;存储这个项目的历史…

算法进阶指南 分形

问题描述 分形&#xff0c;具有以非整数维形式充填空间的形态特征。通常被定义为&#xff1a; “一个粗糙或零碎的几何形状&#xff0c;可以分成数个部分&#xff0c;且每一部分都&#xff08;至少近似地&#xff09;是整体缩小后的形状”&#xff0c;即具有自相似的性质。 现…

18-产品经理-跟踪进度

禅道是一个可以帮助产品经理跟踪研发进度的系统。通过禅道&#xff0c;产品经理可以从多个角度了解产品的研发状态。在仪表盘中&#xff0c;可以展示所有产品或单一产品的概况&#xff0c;包括需求、计划和发布数量&#xff0c;研发需求状态&#xff0c;Bug修复率和计划发布数。…

LeetCode算法题(Go语言实现)_36

题目 给定一个二叉树的根节点 root &#xff0c;和一个整数 targetSum &#xff0c;求该二叉树里节点值之和等于 targetSum 的 路径 的数目。 路径 不需要从根节点开始&#xff0c;也不需要在叶子节点结束&#xff0c;但是路径方向必须是向下的&#xff08;只能从父节点到子节点…

深度解析:文件或目录损坏且无法读取的应对之道

引言 在数字化办公与数据存储日益普及的今天&#xff0c;我们时常会遭遇各种数据问题&#xff0c;其中“文件或目录损坏且无法读取”这一状况尤为令人头疼。无论是个人用户存储在电脑硬盘、移动硬盘、U盘等设备中的重要文档、照片、视频&#xff0c;还是企业服务器上的关键业务…

数据库如何确定或计算 LSN(日志序列号)

目录 如何确定或计算 LSN&#xff08;日志序列号&#xff09;**一、获取当前 LSN****二、确定日志解析的起始 LSN****三、LSN 与物理文件的映射****四、应用场景** 如何确定或计算 LSN&#xff08;日志序列号&#xff09; LSN&#xff08;Log Sequence Number&#xff09;是数…

[ctfshow web入门] web24

前置知识 isset&#xff1a;判断这个变量是否声明且不为NULL&#xff0c;否则返回False mt_srand&#xff1a;设置随机数种子&#xff0c;如果不手动设置&#xff0c;那么系统会自动进行一次随机种子的设置 mt_rand&#xff1a;生成一个随机数&#xff0c;这个随机数与种子有个…

习题与正则表达式

思路&#xff1a; 二分查找&#xff1a; left 1&#xff08;最小可能距离&#xff09;&#xff0c;right L&#xff08;最大可能距离&#xff09;。 每次取 mid (left right) / 2&#xff0c;判断是否可以通过增设 ≤ K 个路标使得所有相邻路标的距离 ≤ mid。 贪心验证…

最小K个数

文章目录 题意思路代码 题意 题目链接 思路 代码 class Solution { public:vector<int> smallestK(vector<int>& arr, int k) {priority_queue<int> Q;for (auto &index:arr){Q.push(index);if (Q.size() > k)Q.pop();}vector<int> ans…

<tauri><rust><GUI>基于rust和tauri,将tauri程序打包为window系统可安装的安装包(exe、msi)

前言 本文是基于rust和tauri,由于tauri是前、后端结合的GUI框架,既可以直接生成包含前端代码的文件,也可以在已有的前端项目上集成tauri框架,将前端页面化为桌面GUI。 发文平台 CSDN 环境配置 系统:windows 10平台:visual studio code语言:rust、javascript库:taur…

SAP系统采购信息记录失效

问题&#xff1a;采购信息记录失效 现象&#xff1a;最初主数据导入完成之后&#xff0c;单元测试的时采购信息记录是有效的&#xff0c;中间经过配置的变化&#xff0c;集成测试初期发现采购信息记录全部失效。 原因&#xff1a; 单元测试时发现采购订单里面的条件类型…

视频分析设备平台EasyCVR打造汽车门店经营场景安全:AI智慧安防技术全解析

一、方案背景 某电动车企业不停爆出维权新闻&#xff0c;支持和反对的声音此起彼伏&#xff0c;事情不断发酵、反转&#xff0c;每天都有新消息&#xff0c;令人目不暇接。车展、车店作为维权事件的高发场所&#xff0c;事后复盘和责任认定时&#xff0c;安防监控和视频监控平…

ecovadis认证基本概述,ecovadis认证审核有效期

EcoVadis认证基本概述 1. 什么是EcoVadis认证&#xff1f; EcoVadis是全球领先的企业可持续发展&#xff08;ESG&#xff09;评级平台&#xff0c;专注于评估企业在**环境&#xff08;E&#xff09;、劳工与人权&#xff08;S&#xff09;、商业道德&#xff08;L&#xff09…

初入Web网页开发

1、网页哪些内容 1.1 三个核心文件的作用 index.html&#xff1a;网页的骨架&#xff0c;用HTML编写网页结构和内容。 script.js&#xff1a;网页的行为&#xff0c;用JavaScript实现交互功能&#xff08;如按钮点击事件&#xff09;。 styles.css&#xff1a;网页的外观&…

CSS 符号

在 CSS 中&#xff0c;& 符号是 嵌套语法中的父选择器引用符&#xff0c;主要用于 CSS 预处理器&#xff08;如 Sass、Less、Stylus&#xff09;和 现代 CSS 嵌套语法&#xff08;CSS Nesting&#xff09;。它代表当前选择器的父级&#xff0c;用于简化嵌套规则并生成更精确…

小白入门JVM、字节码、类加载机制图解

前提知识~ JDK 基本介绍 JDK 的全称(Java Development Kit Java 开发工具包)JDK JRE java 的开发工具[java, javac,javadoc,javap 等]JDK 是提供给Java 开发人员使用的&#xff0c;其中包含了java 的开发工具&#xff0c;也包括了JRE。可开发、编译、调试…… JRE 基本介绍…