🚀 手把手带你手写 Zustand:扒开源码底裤,硬核学习优秀设计思想!

2 阅读11分钟

Hello 各位前端小伙伴们!👋 欢迎来到今天的“硬核源码扒皮”频道!

提到 React 的状态管理,很多同学第一反应可能是那个让人又爱又恨的 Redux。但是近两年,一只可爱的小熊图标 🐻 席卷了 React 社区,它就是 Zustand!凭借其极简的 API、无需 Provider 包裹的便携性以及极佳的性能,Zustand 迅速成为了 React 开发者的心头好。

很多同学会用 Zustand,但一问到原理就只能“阿巴阿巴”。为什么我们需要手写源码? 对 React 学习者来说,手写源码绝对不仅仅是为了在面试官面前装 X(虽然这确实很爽 😎)。更重要的是:

  1. 提升代码能力:跳出日常业务的 CRUD,去看看世界顶尖大佬是如何写出优雅、高复用的代码的。
  2. 学习优秀设计模式:源码里藏着各种神仙设计模式,学会了可以直接搬到你的业务代码里。
  3. 降维打击:当你懂了底层逻辑,再遇到奇怪的 Bug 时,你就能一眼看穿本质,而不是瞎猜。

今天,我们就结合文档和实战代码,以 “手写 Zustand 学习优秀设计思想” 为主题,带你从零撸一个 Zustand,把它的底裤扒得干干净净!🩲✨


🎯 1. 见识一下 Zustand 的魅力:极简的使用姿势

在动手写源码之前,我们先来看看平时是怎么使用 Zustand 的。只有知道了它“长什么样”,我们才能反推它“是怎么做出来的”。

打开我们的测试代码: 首先,我们需要引入 create 方法:

import {
  create // api 
} from 'zustand';

接下来,我们用这个 create 方法来创建一个状态仓库(Store)。 请看这里的代码:

const useXxxStore = create((set,get) => ({
  aaa:"",
  bbb:"",
  updateAaa: (value) => set({ aaa: value }),
  updateBbb: (value) => set({ bbb: value }),
}))

🔥 划重点: 你看,create 方法接收了一个箭头函数作为参数。这个箭头函数自带了一个 set 参数(其实源码里还有一个 get 参数),并且返回了一个对象。这个对象里面不仅包含了我们的状态数据(aaabbb),还包含了修改状态的方法(updateAaaupdateBbb)。

更神奇的是,create 函数执行完毕后,返回的竟然是一个自定义 Hook(useXxxStore)!在 React 组件里,我们只需要给这个 Hook 传入一个函数(也就是 Selector),就能精准地订阅我们想要的状态:

看看组件里是怎么用的:

  const updateAaa = useXxxStore((state) => state.updateAaa);

不仅如此,除了能在 React 组件里作为 Hook 使用,Zustand 甚至还提供了一个 subscribe 方法,允许我们在组件外部或者某些特定场景下,手动去订阅状态的变化,当状态改变时重新执行回调:

    useXxxStore.subscribe((state) => {
      console.log(state.aaa);
    })

是不是特别灵活?这就是 Zustand 的魅力所在!那么,它是怎么做到状态一改变,组件就能精准更新的呢?这就不得不提到前端领域最经典、最伟大的设计模式之一了。


🧠 2. 揭秘核心引擎:经典设计模式之「发布-订阅者模式」

Zustand 能够如此丝滑地管理状态并通知 React 组件更新,底层其实完全依赖于发布-订阅者模式(Publish-Subscribe Pattern)

🎙️ 通俗易懂地解释一下: 想象一下你平时刷抖音或者 B站。

  • Store(仓库) 就是那个网红博主(发布者)
  • React 组件 就是你(订阅者)
  • 当你点击“关注”按钮时,就相当于执行了 subscribe 方法,建立起了订阅关系
  • 当网红博主更新了视频(也就是状态 state 发生了改变),平台就会立刻发通知给你,你的手机就会收到弹窗,然后你就去执行“看视频”的动作(也就是组件重新渲染)。

在这个模式下,发布者(Store)不需要知道具体有哪些组件在使用它,它只需要维护一个“粉丝列表(listeners)”。一旦状态变了,它就挨个通知粉丝列表里的所有人。这就实现了逻辑的完美解耦

理解了这个核心思想,我们的源码之旅就成功了一半。接下来,系好安全带,我们要发车进源码了!🏎️💨


🛠️ 3. 卷起袖子,开始实战源码编写!

我们将视线转移到我们手写的核心文件:zustand.js 中。 我们要实现的代码其实分为两个大块:

  1. 纯 JS 的状态管理核心 (createStore):负责数据的存取和发布订阅,这部分是完全脱离 React 的,哪怕你在 Vue 或者原生 JS 里也能用!
  2. React 的桥梁 (create & useStore):负责把纯 JS 的状态和 React 的生命周期、渲染机制绑定在一起。

📦 模块一:搭建纯粹的状态引擎 createStore

让我们来到源码的第一部分:zustand.js:9-52。 我们的目标是:接收用户传入的箭头函数,将内部的 setget 方法传给用户,完成状态仓库的创建。

1. 状态存储与粉丝列表初始化

看这两行代码:

    let state; // 需要根据createState 初始化状态
    const listeners = new Set();
  • state 就是那个用来保存所有数据的“大金库”。
  • listeners 就是我们的“粉丝列表”。💡 这里有一个极其优秀的细节:为什么要用 Set 而不是数组 [] 因为 Set 天然具有去重功能!在复杂的 React 组件树中,可能会出现同一个组件被多次挂载或重复执行订阅逻辑的情况,使用 Set 可以从数据结构层面完美避免同一个组件重复订阅,防止内存泄漏和重复渲染。

2. 最简单的获取状态方法

    const getState = () => state;

没什么好说的,大道至简,直接把闭包里的 state 返回出去就行。

3. 💥 核心难点:异常强大的 setState 方法

这部分是重头戏:zustand.js:14-31 Zustand 的 set 方法非常强大,它支持三种传参方式

  1. 传对象:setState({ count: 100 }),自动与老状态合并。
  2. 传普通值(不推荐):setState(0),直接覆盖。
  3. 传函数:setState((state) => ({ count: state.count + 1 })),实现基于老状态的函数式更新。

我们来看看源码是怎么把这三种情况吃透的:

首先,判断传入的 partial 是不是函数:

        const nextState = typeof partial === "function" ? partial(state):partial;

如果是函数,就把当前的 state 喂给它执行,返回值作为新的状态 nextState;如果不是函数,那它本身就是 nextState

接着,性能优化的关键一步来了:

        if(!Object.is(nextState,state)) {

💡 硬核知识点:为什么要用 Object.is 而不是 === Object.is 是 ES6 引入的,它在处理 NaN+0/-0 时比 === 更严谨(Object.is(NaN, NaN) 为 true)。如果新老状态完全一样,那就直接 return 掉,什么都不做。这在 React 中叫 Bailout(提前退出),能省下无数次无意义的渲染!

如果状态确实变了,先记录一下旧状态,因为很多订阅者需要对比新旧状态:zustand.js:L21-21

            const previousState = state;

接下来是状态的合并逻辑:zustand.js:23-25

                state = (typeof nextState !== 'object' || nextState === null)
                ?nextState
                :Object.assign({},state,nextState);

如果新的状态不是对象,或者为空,那就直接暴力覆盖(虽然平时不推荐这么写状态)。如果是个对象,就用 Object.assign({}, state, nextState)! 💡 设计思想: 这里的空对象 {} 极其重要,它保证了返回的是一个全新的引用(浅拷贝),符合 React 不可变数据(Immutable) 的原则。

最后,大喇叭广播开始!遍历所有的粉丝(订阅者),让他们全部重新执行更新:zustand.js:L29-29

            listeners.forEach(listener => listener(state,previousState));

4. 订阅机制与初始化

subscribe 方法非常优雅:zustand.js:L34-37

    const subscribe = (listener) => {
        listeners.add(listener);
        return () => listeners.delete(listener);
    }

将传入的函数加入 Set 集合,并且利用闭包,返回一个取消订阅的函数。这就是典型的 React useEffect 清理函数的最佳拍档!

然后我们将所有方法打包成 api 对象

const api = {
        setState,
        getState,
        subscribe,
        destroy
    }

最后,一切准备就绪,执行用户一开始传入的箭头函数,完成状态的首次初始化,并将 api 返回:

    state = createState(setState,getState);

至此,一个脱离 React 也能完美运行的纯 JS 状态机就写好了!👏👏👏


⚛️ 模块二:连接 React 的桥梁 createuseStore

现在有了纯 JS 的金库,我们怎么让 React 组件能用上它,并且在数据变化时自动刷新呢?这就需要写一个高阶函数了。

来看 create 方法:zustand.js:76-89

export const create = (createState) => {
    // 返回 subscribe 方法
    const api = createStore(createState);
    // ...省略

首先第一步,调用我们刚才写的 createStore,拿到那个包含了状态和方法的 api 对象:zustand.js:L78-78

1. 为什么要用 Selector 订阅?

Zustand 的 useXxxStore 要求我们传入一个函数,像这样:1.js:L9-10

const count = useStore((state) => state.count);

为什么要多此一举?直接返回整个 state 不好吗? 💡 硬核知识点: 这正是 Zustand 性能吊打很多状态库的原因!React 的组件非常敏感,状态一变就会重新渲染。如果我们的 Store 里有 100 个状态,而当前组件只用到了 count,如果返回整个 state,另外 99 个状态发生变化时,这个无辜的组件也会被牵连,被迫重新渲染! 通过 (state) => state.count 这种 Selector 机制,组件只订阅自己关心的局部状态,别人怎么变与我无关,性能直接拉满!🚀

所以,create 内部定义了一个接受 selector 的 Hook 函数:zustand.js:L83-85

    const useBoundStore = (selector) => {
        return useStore(api,selector)
    }

2. 见证奇迹的 Hook:useStore 源码剖析

这个 useStore 是整个桥梁的灵魂核心:

在 React 中,普通变量改变是无法触发视图更新的。只有 stateprops 改变才会触发渲染。所以,我们需要一个“强行逼迫 React 刷新”的黑魔法!

    const [,forceRender] = useState(0);

我们声明了一个无意义的 useState(0),不去读取它,只为了拿到 forceRender 方法。只要我们调用它并传入一个随机数,React 就会乖乖重新渲染组件!😈

接着,在 useEffect 里,我们调用 api.subscribe 建立订阅关系: 注意这里的精妙逻辑:

            const newObj = selector(state);// 关心的,改变?不关心 不会改变
            const oldObj = selector(prevState);// 关心的 之前的状态
            if(newObj !== oldObj) {
                forceRender(Math.random()); // 强制组件刷新
            }

当全局大金库(state)发生变化时,网红博主发通知了。但是组件收到通知后没有立刻盲目刷新,而是先把新旧全局状态都喂给用户传入的 selector,拿到“组件关心的局部新状态”和“局部旧状态”。 最后对比一下:if(newObj !== oldObj)。如果我关心的那部分没变?抱歉,我不刷新。如果变了?立刻调用 forceRender(Math.random()) 触发 React 重新渲染页面!

当组件重新渲染时,代码会走到最后一行,返回最新的响应式状态供组件渲染使用:

    return selector(api.getState());

3. JS 奇淫技巧:函数也是对象

回到 create 方法的结尾:

    Object.assign(useBoundStore,api);
    return useBoundStore;

在 JavaScript 中,函数也是一等公民,本质上也是对象!所以我们可以用 Object.assignapi 里面的 getStatesetStatesubscribe 全部挂载到 useBoundStore 这个函数对象上。 这就解释了为什么我们在使用 Zustand 时,既能把 useXxxStore 当作 Hook 函数调用 useXxxStore((state)=>...),又能直接通过点语法去访问它的属性 useXxxStore.subscribe(...) 或者 useXxxStore.getState()。简直是优雅他妈给优雅开门——优雅到家了!🚪✨


🎓 结语:看源码,是破茧成蝶的必经之路

看到这里,恭喜你!你已经亲手撸出了一个简易但核心逻辑完备的 Zustand!🎉

回顾一下,在这短短的代码中,我们学习到了:

  1. 发布-订阅者模式 的完美落地。
  2. 闭包 在状态私有化和取消订阅机制中的高级应用。
  3. 利用 Set 结构防止重复订阅。
  4. React 中 useState 强制刷新组件的黑魔法。
  5. 性能优化中的 Bailout(提前退出) 思想和 Selector 细粒度订阅 机制。
  6. 函数与对象结合的 API 封装技巧。

学习源码,绝不仅仅是为了写出这些代码,而是为了提升代码能力,学习大佬巧妙的设计逻辑,从而更好地理解和使用别人封装好的代码。 当你下一次在项目中敲下 import { create } from 'zustand' 时,你的脑海中一定会浮现出它底层那优雅的齿轮是如何严丝合缝地转动的。

如果你觉得这篇文章对你有帮助,别忘了点赞、收藏、评论三连哦!👍⭐💬 你们的支持是我爆肝更新的最大动力!我们下期再见!👋