引言
在现代 React 开发中,状态管理一直是个绕不开的话题。从 Redux 的繁琐样板代码到 Context API 的性能瓶颈,开发者们一直在寻找更优雅的解决方案。Zustand 正是其中的佼佼者,它以"极简主义"和"高性能"著称。但你有没有想过,这样一个强大的库,其核心原理其实非常简洁?今天,我们就通过一段不到 100 行的代码,亲手实现一个迷你版的 Zustand,揭开它的神秘面纱。
一、核心基石:发布订阅模式与闭包
一切始于 createStore 函数。这是整个状态管理的"心脏"。它的第一个巧妙之处在于利用 JavaScript 闭包 实现了数据的私有化。变量 state 和 listeners 被封闭在函数作用域内,外部无法直接访问或修改,必须通过暴露出的 getState 和 setState 接口。这不仅保证了数据的安全性,也避免了全局污染。
紧接着是 发布订阅模式(Pub/Sub) 的实现。listeners 是一个 Set 集合,用于存储所有关心状态变化的回调函数。当 setState 被调用时,它会先计算出新状态,并通过 Object.is 进行严格比对。只有当状态真正发生变化时,才会遍历 listeners 集合,逐一通知所有订阅者。这种机制实现了数据层与视图层的解耦:数据变了,视图自然知道更新,而无需手动触发。
此外,setState 还展现了极高的灵活性。它支持传入对象直接覆盖,也支持传入函数进行基于旧状态的更新(如 count + 1)。同时,通过 Object.assign 实现的浅合并策略,确保了我们在更新部分状态时,不会丢失其他数据,完美契合了 React 不可变数据的思想。
二、性能关键:选择器与局部渲染
有了仓库,如何让 React 组件高效地连接上它?这就是 useStore Hook 的任务。这里蕴含了 Zustand 高性能的秘密武器:选择器(Selector) 。
传统的 Context API 往往会导致"牵一发而动全身",即任何状态变化都会导致所有消费者组件重渲染。而我们的 useStore 接收一个 selector 函数,允许组件只"订阅"自己关心的那一部分数据。在监听回调中,代码分别对当前状态和上一时刻的状态执行 selector,然后比较结果(newObj !== oldobj)。
只有当组件关心的特定数据片段发生变化时,才会调用 forceRender 触发组件更新。这意味着,如果全局状态中有 10 个字段,但某个组件只依赖其中 1 个,那么其余 9 个字段的变化都不会导致该组件重渲染。这种细粒度的更新控制,是 Zustand 在大型应用中依然保持流畅的关键。
注:在实际生产代码中,useEffect 必须返回一个清理函数来取消订阅,以防止组件卸载后仍保留在监听列表中造成内存泄漏。上述简化代码主要为了突出核心逻辑。
三、架构艺术:高阶函数与 API 挂载
最后,create 函数展示了高超的架构设计技巧。它是一个高阶函数,接收初始状态配置,内部创建唯一的 store 实例,并返回一个特殊的 Hook 函数 useBoundStore。
最精彩的一笔在于 Object.assign(useBoundStore, api)。这行代码将 store 的原始 API(如 getState, setState)直接挂载到了 Hook 函数本身身上。这使得返回的 useCounterStore 具有了"双重身份":
- 作为 Hook 使用 :在组件内部调用
useCounterStore(selector),享受响应式更新和局部渲染优化。 - 作为普通对象使用 :在组件外部、定时器或非 React 环境中,直接调用
useCounterStore.setState()或useCounterStore.getState()。
这种设计极大地降低了学习成本和使用门槛。开发者无需关心 store 实例在哪里,无需传递 provider,只需导入这一个函数,即可在任何地方灵活地读写状态。
完整代码实现
以下是完整的、可直接运行的迷你 Zustand 实现代码。你可以将其保存为 mini-zustand.js 并在你的 React 项目中引用。
ini
import { useEffect, useState } from "react";
/**
* 1. createStore: 核心仓库工厂
* 负责创建私有的状态空间和管理订阅者列表
*/
const createStore = (createState) => {
let state; // 闭包变量,存储真实状态,外部不可直接访问
const listeners = new Set(); // 订阅者集合 (Set 自动去重)
// 获取当前状态
const getState = () => state;
// 修改状态的核心方法
// partial: 新状态片段 或 更新函数
// replace: 是否完全替换整个状态 (默认 false,即合并)
const setState = (partial, replace = false) => {
// 支持函数式更新:(prev) => ({ count: prev.count + 1 })
const nextState = typeof partial === 'function' ? partial(state) : partial;
// 优化:如果新旧状态完全一致 (Object.is),则不执行任何操作
if (!Object.is(nextState, state)) {
const previousState = state;
// 如果不是替换模式,且新状态是对象,则进行浅合并
if (!replace && (typeof nextState === 'object' && nextState !== null)) {
state = Object.assign({}, state, nextState);
} else {
// 否则直接赋值 (基本类型 或 替换模式)
state = nextState;
}
// 通知所有订阅者:传入新状态和旧状态
listeners.forEach(listener => listener(state, previousState));
}
};
// 订阅方法:添加监听器,并返回取消订阅的函数
const subscribe = (listener) => {
listeners.add(listener);
// 返回取消订阅函数,防止内存泄漏的关键
return () => listeners.delete(listener);
};
// 销毁方法:清空所有监听器
const destroy = () => {
listeners.clear();
};
// 暴露 API
const api = {
getState,
setState,
subscribe,
destroy,
};
// 初始化状态:调用用户提供的 createState 函数
state = createState(setState, getState);
return api;
};
/**
* 2. useStore: React Hook 连接器
* 负责将组件订阅到仓库,并实现基于 Selector 的局部渲染优化
*/
const useStore = (api, selector) => {
// 创建一个仅用于触发重渲染的状态,不需要具体值
const [, forceRender] = useState(0);
useEffect(() => {
// 定义监听回调函数
const listener = (state, prevState) => {
// 提取组件关心的新数据
const newObj = selector(state);
// 提取组件关心的旧数据
const oldObj = selector(prevState);
// 核心优化:只有当关心的数据发生变化时,才触发重渲染
if (newObj !== oldObj) {
forceRender(prev => prev + 1);
}
};
// 订阅状态变化
const unsubscribe = api.subscribe(listener);
// 【重要】清理函数:组件卸载时自动取消订阅,防止内存泄漏
return () => {
unsubscribe();
};
}, [api, selector]); // 依赖项:当 api 或 selector 变化时重新订阅
// 返回当前选中的状态数据
return selector(api.getState());
};
/**
* 3. create: 高级工厂函数
* 封装 createStore 和 useStore,返回一个既可做 Hook 又可做普通对象使用的函数
*/
export const create = (createState) => {
// 创建唯一的 store 实例 (闭包保护,单例模式)
const api = createStore(createState);
// 定义绑定了特定 api 的 Hook 函数
const useBoundStore = (selector) => {
// 如果没有传 selector,默认返回整个状态 (兼容某些用法)
if (!selector) return api.getState();
return useStore(api, selector);
};
// 【黑魔法】将 api 的方法 (setState, getState 等) 直接挂载到 Hook 函数上
// 这样 useBoundStore 既可以当函数用,也可以 useBoundStore.setState() 调用
Object.assign(useBoundStore, api);
return useBoundStore;
};
结语
通过这段代码,我们看到了 Zustand 的灵魂:闭包封装数据、发布订阅驱动更新、选择器优化性能、高阶函数简化调用。它证明了优秀的状态管理库不需要复杂的样板代码,只需要对 JavaScript 基础特性的深刻理解和巧妙运用。手写这样一个迷你版,不仅能帮助我们彻底理解 Zustand 的原理,更能让我们在面对复杂应用架构时,拥有更清晰的设计思路。