深入浅出:手写一个迷你版 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 的原理,更能让我们在面对复杂应用架构时,拥有更清晰的设计思路。

相关推荐
gustt1 小时前
手写 Zustand:从零实现 React 轻量级状态管理库
前端·面试
读忆2 小时前
在前端开发中使用组件后, 若是出了bug, 应该如何排查, 怎么排查, 解决方式是什么?
前端·javascript·vue.js·bug
We་ct2 小时前
LeetCode 162. 寻找峰值:二分高效求解
前端·算法·leetcode·typescript·二分·暴力
HWL56792 小时前
uni-app的生命周期
前端·vue.js·uni-app
softbangong2 小时前
829-批量提取各子文件夹下文件到一级目录
java·服务器·前端·自动化工具·批量文件处理·文件提取工具·文件夹整理
李剑一2 小时前
别再瞎写 Cesium 可视化!热力图 + 四色图源码全公开,项目直接复用!
前端·vue.js·cesium
SuperEugene2 小时前
Vue3 + Vue Router + Pinia 路由守卫规范:beforeEach 应做 / 不应做,避死循环、防重复请求|状态管理与路由规范篇
开发语言·前端·javascript·vue.js·前端框架
Greg_Zhong2 小时前
Css知识之伪类和伪元素
前端·css
Mintopia2 小时前
GPT-5.3-Codex 底层逻辑是什么,为什么编码强?
前端·人工智能·ai编程