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