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.pswp.cn/diannao/77178.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/77178.shtml
英文地址,請注明出處:http://en.pswp.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 基本介紹…