深入浅出:手写一个迷你版 Zustand

引言

在现代 React 开发中,状态管理一直是个绕不开的话题。从 Redux 的繁琐样板代码到 Context API 的性能瓶颈,开发者们一直在寻找更优雅的解决方案。Zustand 正是其中的佼佼者,它以"极简主义"和"高性能"著称。但你有没有想过,这样一个强大的库,其核心原理其实非常简洁?今天,我们就通过一段不到 100 行的代码,亲手实现一个迷你版的 Zustand,揭开它的神秘面纱。

一、核心基石:发布订阅模式与闭包

一切始于 createStore 函数。这是整个状态管理的"心脏"。它的第一个巧妙之处在于利用 JavaScript 闭包 实现了数据的私有化。变量 statelisteners 被封闭在函数作用域内,外部无法直接访问或修改,必须通过暴露出的 getStatesetState 接口。这不仅保证了数据的安全性,也避免了全局污染。

紧接着是 发布订阅模式(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 具有了"双重身份":

  1. 作为 Hook 使用 :在组件内部调用 useCounterStore(selector),享受响应式更新和局部渲染优化。
  2. 作为普通对象使用 :在组件外部、定时器或非 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 的原理,更能让我们在面对复杂应用架构时,拥有更清晰的设计思路。

相关推荐
weixin_382395231 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__1 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.2 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~3 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华4 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子6 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅6 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni7 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学8 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端