跳到主要内容

实现

2025年03月05日
柏拉文
越努力,越幸运

一、认识


Zustand 相较于 React-Redux 的优势在于其极简且直观的 API 设计,无需繁琐的 actionreducer 和中间件配置, 无需繁杂的样板代码, 通过闭包和订阅机制实现了精准更新, 确保组件只在真正需要时才重新渲染, 同时支持状态切片和选择器,使得组件能够只订阅所需数据。再加上与 React 18useSyncExternalStore 无缝衔接, 能够在并发模式下保持数据一致性和高性能, 借助 useSyncExternalStore 与选择器使组件仅订阅所需状态,从而减少不必要的重渲染。同时它不依赖于 React Context,避免了大规模状态更新时的性能瓶颈,使用起来更符合现代 React Hooks 编程风格,学习曲线更低,适用于从小型到大型应用,而 Redux 尽管拥有丰富的生态系统和强大的中间件支持,但往往需要更多样板代码和复杂的架构来管理状态。

useSyncExternalStoreReact 18 引入的一个内置 Hook, 专门用于解决外部数据源(比如 Redux``、Zustand 等状态管理库)与 React 组件之间的数据同步问题。它的主要作用是让组件在渲染期间能够安全、一致地读取外部 store 的状态快照, 同时在 store 发生变化时,自动触发组件重新渲染,从而确保 UI 始终与外部状态保持同步。具体来说,它需要你提供两个核心函数:

  • subscribe: 一个函数, 接收一个单独的 callback 参数并把它订阅到 store 上。当 store 发生改变时会调用提供的 callback,这将导致 React 重新调用 getSnapshot 并在需要的时候重新渲染组件。subscribe 函数会返回清除订阅的函数。换句话说: subscribe 的作用就是让每个使用 store 的组件都能注册自己的订阅回调。当 store 更新时, 我们手动调用订阅回调, 从而让相应的组件通过 getSnapshot 获取最新状态并触发重新渲染。所以 subscribe 注册的回调数目实际上就反映了当前有多少组件(或其他订阅者)在关注这个 store 的变化,同时它也支持取消订阅以避免不必要的更新。

  • getSnapshot: 一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用 getSnapshot 必须返回同一个值。如果 store 改变,并且返回值也不同了(用 Object.is 比较),React 就会重新渲染组件。

  • 返回值: 该 store 的当前快照,可以在你的渲染逻辑中使用。

Zustandcreate 实现逻辑基于 useSyncExternalStore, 通过 subscribe 参数来收集所有订阅该 Store 的组件的通知更新回调。通过 getSnapshot 来传递给 store 当前数据状态。当修改 store 数据后, 会循环遍历订阅 Store 的组件通知更新回调, React 会重新调用 getSnapshot , 并通过 Object.is 来比较前后状态, 如果发生变化, 在需要的时候重新渲染组件。因此, Zustandcreate 通过 useSyncExternalStore subscribe 与 getSnapshot 的结合, 实现组件对外部状态的精准订阅和更新, 从而构建出一个轻量高效且易于扩展的状态管理解决方案。

二、实现


import { useSyncExternalStore } from "react";

const createStore = (createState) => {
let state;
const listeners = new Set();

const setState = (partial, replace) => {
const nextState = typeof partial === "function" ? partial(state) : partial;

if (!Object.is(nextState, state)) {
const previousState = state;
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 api = { setState, getState, getInitialState, subscribe };
const initialState = (state = createState(setState, getState, api));
return api;
};

function useStore(api, selector) {
const slice = useSyncExternalStore(
api.subscribe,
() => selector(api.getState()),
() => selector(api.getInitialState())
);
return slice;
}

function create(createState) {
const api = createStore(createState);
const useBoundStore = (selector) => useStore(api, selector);
Object.assign(useBoundStore, api);
return useBoundStore;
}

三、测试


import { create } from "./create";

const useCountStore = create((set, get) => ({
count: 0,
setCount: (count) => set({ count }),
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
}));

export default useCountStore;
import useCountStore from "./store/zustand/index1";

function App() {
const count = useCountStore((state) => state.count);
const increment = useCountStore((state) => state.increment);
const decrement = useCountStore((state) => state.decrement);

return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increase</button>
<button onClick={decrement}>Decrease</button>
</div>
);
}

export default App;