前言
React 状态管理库层出不穷,从 Redux、MobX 到 Recoil、Jotai、Zustand,每一个都号称能解决你的痛点。但在这么多选择里,Zustand 凭借极简的 API 和不到 1KB 的体积,在 GitHub 上狂揽 50k+ star,成为了当下最流行的轻量级状态管理方案。
js体验AI代码助手代码解读复制代码// Zustand 有多简单?三行搞定全局状态 const useStore = create((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), }));
我一直觉得,真正理解一个工具,不是看文档背 API,而是把它拆开看看里面到底是怎么转的。今天我们就从零开始,一步一步手写一个 Zustand。
读完这篇文章,你会彻底搞懂三件事:
- 状态管理库怎么「存」状态
- 发布订阅模式怎么驱动 React 组件「响应式」更新
-
create函数和自定义 Hook 是怎么封装出来的
我们先把官方的 Zustand 放一边,从一个最简单的 store 开始写起。
第一步:一个最简陋的 store
抛开所有概念,状态管理说到底就三个操作—— 存、取、改。
js体验AI代码助手代码解读复制代码// zustand2.js —— 最朴素的版本 export function createStore() { let state = { count: 0 }; const getState = () => state; const setState = (newState) => { state = newState; }; return { getState, setState, }; }
来用一下,
js体验AI代码助手代码解读复制代码// App3.jsx import { createStore } from './zustand'; export default function App() { const store = createStore(); console.log(store.getState()); // { count: 0 } store.setState({ count: 10 }); console.log(store.getState()); // { count: 10 } }
就这么简单。一个闭包把
state 包起来,外面只能通过
getState 和
setState 访问,避免了直接篡改。
闭包是 JavaScript 里最朴素的信息隐藏机制——外部函数作用域里的变量,内部函数可以一直访问,但外部代码摸不着。
但问题来了:改了状态,谁通知我呢?组件怎么知道数据变了要重新渲染?
第二步:发布订阅——让状态变更「可被感知」
Zustand 的核心机制就是 发布订阅模式。store 是「发布者」,组件是「订阅者」。状态变了,store 挨个通知所有订阅者——「嘿,数据更新了,你们该干嘛干嘛」。
js体验AI代码助手代码解读复制代码// zustand.js —— 加入订阅机制 export function createStore() { let state = { count: 0 }; // Set 确保同一个订阅者不会被重复添加 const listeners = new Set(); const getState = () => state; const setState = (newState) => { state = newState; // 通知所有订阅者 listeners.forEach((listener) => listener()); }; const subscribe = (listener) => { listeners.add(listener); // 返回一个取消订阅的函数 return () => listeners.delete(listener); }; return { getState, setState, subscribe, }; }
注意两个细节。
第一个,
listeners 用的是
Set 而不是数组。因为同一个组件可能会多次调用
subscribe,用数组的话会存进去重复的函数,通知的时候同一个组件被触发多次,白白浪费渲染。
Set 天然去重,完美解决。
第二个,
subscribe 返回了一个
() => listeners.delete(listener)。这是订阅模式的标配——组件卸载的时候得把订阅取消掉,不然组件都没了,store 还在那喊着让你更新,内存泄漏就是这么来的。
来试一下,
js体验AI代码助手代码解读复制代码// App.jsx import { createStore } from './zustand'; export default function App() { const store = createStore(); // 订阅者 A store.subscribe(() => { console.log(`A 收到通知,最新 count: ${store.getState().count}`); }); // 订阅者 B store.subscribe(() => { console.log(`B 收到通知,最新 count: ${store.getState().count}`); }); store.setState({ count: 10 }); // 控制台输出: // A 收到通知,最新 count: 10 // B 收到通知,最新 count: 10 }
到这里,一个带订阅机制的状态容器已经有了。但现在的用法跟 React 组件还是割裂的——你得手动
subscribe,手动
getState,跟写原生 JS 似的。
Zustand 真正好用的地方,是它把 store 包装成了一个自定义 Hook,组件里直接
useXxxStore(state => state.count) 就能拿到数据,而且状态变了组件自动重渲染。接下来我们就实现这个。
第三步:create 函数——把 store 变成 Hook
回想一下 Zustand 的真实用法,
js体验AI代码助手代码解读复制代码const useXxxStore = create((set) => ({ aaa: '', bbb: '', updateAaa: (value) => set({ aaa: value }), }));
create 接收一个初始化函数,返回一个自定义 Hook
useXxxStore。这个 Hook 既可以从 store 里取数据,又自带订阅能力(状态变了自动触发组件重渲染)。
我们来拆解
create 的实现思路:
- 内部调
createStore拿到 store 实例 - 执行用户传入的初始化函数,把返回值作为初始状态
- 暴露
set函数——接收部分状态,跟旧状态合并(不是直接覆盖) - 返回一个 Hook
useStore,内部用useSyncExternalStore订阅 store 变化 -
useStore支持传入 selector 函数来取局部状态
js体验AI代码助手代码解读复制代码import { useSyncExternalStore } from 'react'; export function create(createState) { // 底层还是我们之前写的 createStore const store = createStore(); // 用户传入的 set 函数:接收部分状态,合并后写入 const set = (partial) => { const nextState = partial(store.getState()); store.setState(nextState); }; // 执行用户传入的初始化函数,拿到初始状态 const initialState = createState(set); store.setState(initialState); // 返回一个自定义 Hook function useStore(selector) { // useSyncExternalStore 是 React 18 专门给外部状态管理库开的后门 // 它帮你搞定两件事:订阅 store 变化 + 触发组件重渲染 return useSyncExternalStore( store.subscribe, // React 帮你调用 subscribe () => selector(store.getState()) // 取当前状态 ); } return useStore; }
这里面最关键的一行是
useSyncExternalStore。
React 18 之前,想做一个外部的状态管理库让组件响应式更新,得自己折腾
useState、
useEffect、
forceUpdate 各种黑魔法,稍不注意就有 tearing 问题(同一个状态在同一个渲染帧里读到不同的值)。
React 18 直接给了
useSyncExternalStore,专门解决「外部 store 怎么接入 React 渲染周期」这个问题。第一个参数传
subscribe,第二个参数传
getSnapshot,React 帮你处理剩下的一切。
第四步:完善细节
上面的
create 还差点意思,我们来把剩下的边边角角补齐。
get 方法和 store API 暴露:
create 的回调里,用户除了
set,还可能需要
get(直接读状态)和
store(拿到 store 实例本身),
js体验AI代码助手代码解读复制代码export function create(createState) { const store = createStore(); const set = (partial) => { const nextState = partial(store.getState()); store.setState(nextState); }; const get = () => store.getState(); // 把 set、get、store 都传给用户 const initialState = createState(set, get, store); store.setState(initialState); function useStore(selector) { if (!selector) { // 不传 selector,默认返回整个状态 return useSyncExternalStore( store.subscribe, () => store.getState() ); } return useSyncExternalStore( store.subscribe, () => selector(store.getState()) ); } return useStore; }
selector 的浅比较优化:
目前每次 store 里任何状态变化,所有用到
useStore 的组件都会重渲染。真实 Zustand 里,
useStore 默认会用浅比较来判断 selector 的返回值是不是真的变了,没变就不触发重渲染。这样用
useXxxStore(state => state.count) 的组件,在
aaa 变化时就不会重渲染。
这里就不展开了,感兴趣的同学可以去看 Zustand 源码里
useSyncExternalStoreWithSelector 的实现。
完整的实现
把上面的代码合并起来,就是我们的最终版本,
js体验AI代码助手代码解读复制代码import { useSyncExternalStore } from 'react'; // 底层 store:存状态、改状态、订阅 function createStore() { let state = { count: 0 }; const listeners = new Set(); const getState = () => state; const setState = (newState) => { state = newState; listeners.forEach((listener) => listener()); }; const subscribe = (listener) => { listeners.add(listener); return () => listeners.delete(listener); }; return { getState, setState, subscribe }; } // 上层 create:封装成 Hook,接入 React 渲染周期 export function create(createState) { const store = createStore(); const set = (partial) => { const nextState = partial(store.getState()); store.setState(nextState); }; const get = () => store.getState(); const initialState = createState(set, get, store); store.setState(initialState); function useStore(selector) { if (!selector) { return useSyncExternalStore(store.subscribe, () => store.getState()); } return useSyncExternalStore(store.subscribe, () => selector(store.getState()) ); } return useStore; }
总共不到 40 行代码,核心就三层:
| 层 | 职责 | 关键技术 |
|---|---|---|
createStore |
存取状态 + 发布订阅 | 闭包 +
Set + 观察者模式 |
create |
封装成 Hook 接入 React |
useSyncExternalStore |
| selector | 按需取值 + 渲染优化 | 函数式编程(把函数当参数传) |
我们从中学到了什么
手写 Zustand 的过程,本质上是一次对 React 状态管理范式的拆解。
你会发现,所谓「状态管理库」,核心并不神秘——闭包存数据,发布订阅做通知,React 18 的
useSyncExternalStore 搭桥接入渲染周期。这三板斧搭起来,任何一个前端都能在半天之内写出自己的状态管理方案。
真正让 Zustand 流行起来的,不是它用了什么高深的技术,恰恰相反,是它的「足够简单」。没有 Provider 包裹、没有 action 类型枚举、没有中间件配置,就是一个函数,一个 Hook,完事。
下次在项目里遇到状态管理的需求,不妨想想你是不是真的需要搬出全套 Redux Toolkit。很多时候,理解了原理之后,几十行代码就能解决的问题,不值得引入几万行的依赖。
作者:不会敲代码1
链接:https://juejin.cn/post/7639624625789878287
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。