手写 Zustand:三十分钟带你搞懂状态管理库的核心原理

前言

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 包起来,外面只能通过 getStatesetState 访问,避免了直接篡改。

闭包是 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 的实现思路:

  1. 内部调 createStore 拿到 store 实例
  2. 执行用户传入的初始化函数,把返回值作为初始状态
  3. 暴露 set 函数——接收部分状态,跟旧状态合并(不是直接覆盖)
  4. 返回一个 Hook useStore,内部用 useSyncExternalStore 订阅 store 变化
  5. 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 之前,想做一个外部的状态管理库让组件响应式更新,得自己折腾 useStateuseEffectforceUpdate 各种黑魔法,稍不注意就有 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


请使用浏览器的分享功能分享到微信等