我們一起聊聊70 行代碼實現 Zustand 核心功能
前端目前主流的開發技術棧如 React、Vue 等都是狀態數據驅動 UI 更新(即 UI = f(state)),所以狀態管理是項目開發的重要一環。
React 和 Vue 除了自帶的狀態管理 API,同時還有一些功能強大的狀態管理庫可供選擇。Vue 常見的狀態管理庫有 Vuex 和 Pinia,React 狀態管理相對更多,有 redux、mobox、zustand、jotai 等等。
在 React 中,redux 還是最熱門的狀態管理庫,相信你肯定在 React 開發中有使用過它。其他的狀態庫,都有各自的設計理念,在某些場景和開發規范,它們可能更適合你的項目。
本文將介紹 zustand 的核心實現,zustand 庫和 redux 類似,都參考了 flux 設計理念,它一些特點如下:
- 易于上手,學習成本低
- 輕量級設計,gzip 壓縮后僅 1KB
- TypeScript 友好,有助于提升代碼質量和開發體驗
- 強大的可擴展性,通過中間件可以實現日志,數據持久化等能力
- zustand 在設計上注重性能,采用高效的更新機制減少不必要的渲染,同時支持狀態分片。
基于上述特點,zustand 還是比較受歡迎的,你可以看到 zustand 的使用量是排在前頭的。
圖片
Zustand 的使用
zustand 的使用起來很簡單, 使用 create 創建一個 useStore,可以把狀態值和更新狀態函數都保存在 state 中,隨后在組件中調用即可。
import { create } from 'zustand'
const useStore = create((set) => ({
count: 1,
// 通過 set 方法更新狀態值,set 支持傳入函數和狀態對象值
inc: () => set((state) => ({ count: state.count + 1 })),
}))
function Counter() {
const count = useStore((state) => state.count)
const inc = useStore((state) => state.inc)
return (
<div>
<p>{`Count: ${count}`}</p>
<button onClick={inc}>+1</button>
</div>
)
}
代碼體驗地址:https://code.juejin.cn/pen/7396472908036210698
同時 zustand 核心代碼也可以在普通 JS 中調用,把上述功能用普通 JS 實現就如下:
<div>
<p>Count: <span id="value"></span></p>
<button id="btn">+1</button>
</div>
import { createStore } from 'zustand@4.5.4/vanilla'
const store = createStore((set) => ({
count: 1,
inc: () => set((state) => ({ count: state.count + 1 })),
}))
const { getState, setState, subscribe, getInitialState } = store
window.onload = () => {
const value = document.querySelector('#value')
value.innerHTML = getInitialState().count // 設置 store 中 count 值
// 使用 subscribe 訂閱狀態變化,并更新數值
subscribe((state) => {
value.innerHTML = state.count
})
const btn = document.querySelector('#btn')
btn.onclick = () => {
// 觸發更新
getState().inc()
}
}
代碼體驗地址:https://code.juejin.cn/pen/7396483548833644581
Zustand 的實現
Vanilla 版本
zustand 的核心實現非常簡潔,我們先實現一個普通版本的 zustand,因為 react hook 版本也需要使用到它。從上面zustand 使用案例代碼可以看出,state 狀態值不能直接修改,要通過 setState 來觸發修改,這個和 redux 一致,對于通知狀態變化則使用了發布訂閱模式。
核心實現大概如下:
圖片
const create = (createState) => {
let state
let initialState
const listeners = new Set()
const setState = (partial, replace) => {
// 判斷是否為函數,為函數就調用,并傳入當前狀態值
const nextState = typeof partial === 'function'
? partial(state)
: state
// 對比狀態值是否有變化
if (!Object.is(nextState, state)) {
const previousState = state
// 如果是替換整個狀態值,或者狀態值為基礎值或 null,則直接賦值,不然使用 Object.aasign 合并狀態值
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 destory = () => listeners.clear()
const api = {
setState,
getState,
getInitialState,
subscribe,
destory
}
// 調用 createState,createState 參數為 set、get 和 api 對象,函數返回狀態初始值
initialState = (state = createState(setState, getState, api))
return api
}
export default create
代碼體驗地址:https://code.juejin.cn/pen/7396500100421255204
React Hook
接著基于普通版本實現 React Hook 版本。在實現前,我們先了解一個 React 自帶的 Hook - useSyncExternalStore[1]。
useSyncExternalStore 的作用是讓你可以訂閱外部的狀態源,當外部狀態源發生變化時,React 會觸發重選渲染。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
useSyncExternalStore 調用第一個參數 subscribe 訂閱數據源變化,當數據源變化了,就觸發重新渲染,并調用 getSnapshot 返回最新的狀態值。
有了這個 Hook 的支持,我們就可以輕松實現 React Hook 版本 zustand。
import createImpl from './vanilla'
import useSyncExternalStoreExports from 'use-sync-external-store/shim/with-selector'
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports
const create = (createState) => {
// 使用普通版本 zustand 創建一個支持發布訂閱的數據源
const api = createImpl(createState)
// zustand 版本 Hook,參數為狀態值選擇器和判斷狀態是否變化函數
const useBearStore = (selector, equiltyFn?) =>
// 和 useSyncExternalStore 類似,不過支持傳入 selector,獲取部分數據
useSyncExternalStoreWithSelector(
api.subcribe,
api.getState,
api.getInitialState,
selector,
equiltyFn,
)
// 把 api 合并到 Hook 對象上
Object.assign(useBearStore, api)
return useBearStore
}
export default create
至此我們已經完成了 zustand 核心功能的代碼編寫。
代碼體驗:https://code.juejin.cn/pen/7396503370447781951
拓展
useSyncExternalStoreWithSelector
React Hook 版本的 zustand 中使用到了 useSyncExternalStoreWithSelector[2],這個 Hook 是基于 useSyncExternalStore 實現的,可以簡單了解下它的實現,簡化版源碼如下(去除了服務端渲染等內容):
// 相比于 useSyncExternalStore ,多了 selector 和 isEqual 參數
function useSyncExternalStoreWithSelector(
subscribe,
getSnapshot,
getServerSnapshot,
selector,
isEqual?,
) {
const [getSelection, getServerSelection] = useMemo(() => {
let memoizedSnapshot; // 緩存的整個狀態值
let memoizedSelection: Selection; // 緩存的使用 selector 選中的部分狀態值
const memoizedSelector = (nextSnapshot: Snapshot) => {
const prevSnapshot = memoizedSnapshot
const prevSelection = memoizedSelection
// 如果整體狀態值相等,直接返回緩存的 selector 選中的狀態值。
if (is(prevSnapshot, nextSnapshot)) {
return prevSelection;
}
// 使用 selector 函數獲取最新的狀態值
const nextSelection = selector(nextSnapshot);
// 有傳入判斷狀態是否相等函數,相等的話就返回上次的 selector 選中值。
if (isEqual !== undefined && isEqual(prevSelection, nextSelection)) {
// 記錄最新的整體狀態值
memoizedSnapshot = nextSnapshot;
return prevSelection;
}
// 記錄最新一次的更新值
memoizedSnapshot = nextSnapshot;
memoizedSelection = nextSelection;
// 返回 selector 函數獲取最新的狀態值
return nextSelection;
};
const getSnapshotWithSelector = () => memoizedSelector(getSnapshot());
return [getSnapshotWithSelector, () => {}];
}, [getSnapshot, getServerSnapshot, selector, isEqual]);
// 調用 useSyncExternalStore 方法,第二參數不是整體獲取整個狀態值,而是 selector 的狀態值
const value = useSyncExternalStore(
subscribe,
getSelection,
getServerSelection,
);
return value
}
總結
可以看到 zustand 核心代碼還是很簡潔的。通過實現核心代碼,我們可以更好地理解和使用 zustand。有興趣的同學可以繼續了解下 zustand 插件相關的內容。
參考資料
[1]useSyncExternalStore: https://react.dev/reference/react/useSyncExternalStore
[2]useSyncExternalStoreWithSelector: https://github.com/facebook/react/blob/d17e9d1ce566276fc54a8ea27f4e9ea1fa434e62/packages/use-sync-external-store/src/useSyncExternalStoreWithSelector.js