引言
在上一篇《探索 Antd Form 表单实现原理》中,我们深入剖析了表单状态管理的核心机制,文末提到了第三方状态管理库的设计思想。这些思想与 Redux、Zustand 等主流状态管理库,以及 React 18 中新引入的 useSyncExternalStore
钩子可谓异曲同工,都围绕着同一个核心问题:如何优雅地在 React 组件树中共享和同步状态?
React 18 的 useSyncExternalStore
的推出,标志着 React 官方对第三方状态管理库的正式认可和标准化。这个看似简单的 Hook,实际上解决了并发渲染下外部状态同步的复杂问题,为像 Zustand 这样的状态管理库提供了坚实的基础。
今天,我们将深入 Zustand 的源码实现,探究它是如何基于 feat: use-sync-external-store 构建出一个既简洁又强大的状态管理方案。从发布订阅模式的经典实现,到 React 18 并发特性的完美适配。
基本使用
在正式深入源码解析之前,我们先来感受一下 Zustand 的魅力。官方提供了一个非常精美的 交互式演示,通过这个生动的 demo,你可以直观地体验 Zustand 的核心特性和使用场景。
推荐学习路径:
🎯 第一步:体验实战 - 通过 官方演示 感受 Zustand 的简洁优雅
🔍 第二步:掌握基础 - 阅读 GitHub README 了解核心 API 和最佳实践
📚 第三步:深入学习 - 查阅 官方文档 探索高级用法和设计理念
这种由浅入深的学习方式,能够帮你快速掌握 Zustand 的基本使用,为接下来的源码探索打下坚实基础。毕竟,只有真正理解了 API 的设计意图,才能更好地理解其底层实现。
基础
首先我们要知道zustand 提供了什么能力。官方 GitHub README 下面提到了两种使用方式:
在 react 中使用:
js
import { create } from 'zustand'
const useBearStore = create((set) => ({
bears: 0,
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
function BearCounter() {
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
在原生 js 中使用
js
import { createStore } from 'zustand/vanilla'
const store = createStore((set) => ...)
const { getState, setState, subscribe, getInitialState } = store
export default store
Zustand 源码
Zustand 的源码是用 TypeScript 编写的,但类型定义可能会影响代码的阅读体验。对于这类 TypeScript 项目,我们可以选择阅读打包后的代码(TypeScript 编译器会将 .ts 文件编译为 .js 文件和对应的 .d.ts 类型声明文件)。
核心逻辑层
Zustand 的核心逻辑层(vanilla)实现了纯粹的状态管理机制,完全独立于任何前端框架。让我们深入探索这个精心设计的核心实现。
完整源码
js
// 打包后的 vanilla.js
"use strict";
const createStoreImpl = (createState) => {
let state;
const listeners = /* @__PURE__ */ new Set();
const setState = (partial, replace) => {
const nextState = typeof partial === "function" ? partial(state) : partial;
if (!Object.is(nextState, state)) {
const previousState = state;
state = (
replace != null
? replace
: typeof nextState !== "object" || nextState === null
)
? nextState
: Object.assign({}, state, nextState);
listeners.forEach((listener) => listener(state, previousState));
}
};
const getState = () => state;
const getInitialState = () => initialState;
const subscribe = (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
};
const api = { setState, getState, getInitialState, subscribe };
const initialState = (state = createState(setState, getState, api));
return api;
};
const createStore = (createState) =>
createState ? createStoreImpl(createState) : createStoreImpl;
exports.createStore = createStore;
核心设计思路
如果我是 Zustand 的作者,我首先会问自己:状态管理的本质究竟是什么?
剥离掉 React、Vue 这些框架的外衣,状态管理其实就是三件事:
- 存储状态 - 在内存中维护数据
- 更新状态 - 提供修改数据的机制
- 通知变化 - 让关心状态的地方知道变化
js
// 这就是状态管理的本质抽象
const createStoreImpl = (createState) => {
let state; // 存储:闭包变量保存状态
const listeners = new Set(); // 通知:订阅者管理
const setState = (partial) => { ... } // 更新:状态修改接口
const subscribe = (listener) => { ... } // 订阅:变化监听机制
}
js
const listeners = /* @__PURE__ */ new Set();
为什么用 Set
而不是数组?
作为作者,我考虑过重复订阅的边界情况。如果一个组件因为某种原因多次订阅了同一个状态,用数组可能会导致重复通知,影响性能甚至引发 bug。而 Set
天然具备去重特性,优雅地解决了这个问题。
注释 /* @__PURE__ */
告诉打包工具这是无副作用的操作,便于 tree-shaking 优化。
❗状态更新机制
js
const setState = (partial, replace) => {
这个函数签名体现了作者对 API 设计的深度思考:
partial
:语义上暗示"部分更新",这是大多数场景的需求replace
:处理完全替换的特殊需求,如状态重置或服务器数据同步
js
const nextState = typeof partial === "function" ? partial(state) : partial;
这一行代码巧妙地统一了两种更新模式:
js
// 模式1:直接传值 - 简单直观
setState({ count: 5 })
// 模式2:函数式更新 - 解决闭包陷阱
setState(state => ({ count: state.count + 1 }))
为什么要支持函数式更新?
这是借鉴了 React setState
的经验教训。在异步操作或连续更新中,直接使用变量值容易产生闭包陷阱:
js
// 🚫 危险:可能基于过期的状态
setTimeout(() => {
setState({ count: currentCount + 1 }); // currentCount 可能已经过期
}, 1000);
// ✅ 安全:总是基于最新状态
setTimeout(() => {
setState(state => ({ count: state.count + 1 })); // state 永远是最新的
}, 1000);
变化检测
js
if (!Object.is(nextState, state)) {
作者选择 Object.is()
而非简单的 ===
比较,这体现了对边界情况的细致考虑:
js
// 边界情况对比
Object.is(NaN, NaN) // true - NaN 被认为相同,不触发更新
NaN === NaN // false
Object.is(+0, -0) // false - 正负零被认为不同,会触发更新
+0 === -0 // true
历史状态的保存
js
const previousState = state;
- 订阅者需求:许多场景需要对比新旧状态
- 调试支持:开发工具可以展示状态变迁历史
状态更新策略的分层逻辑
js
state = (
replace != null
? replace
: typeof nextState !== "object" || nextState === null
)
? nextState
: Object.assign({}, state, nextState);
这个看似复杂的三元表达式,实际上体现了清晰的优先级逻辑:
js
// 优先级解析
if (replace != null) {
// 1. 显式替换模式 - 最高优先级
state = replace;
} else if (typeof nextState !== "object" || nextState === null) {
// 2. 基本类型自动替换 - 无法合并的情况
state = nextState;
} else {
// 3. 对象合并模式 - 默认行为
state = Object.assign({}, state, nextState);
}
设计场景覆盖:
js
// 场景1:显式替换
setState({ count: 1 }, true) // 完全替换,忽略合并
// 场景2:基本类型替换
setState(42) // 状态变成数字 42
setState("hello") // 状态变成字符串 "hello"
setState(null) // 状态重置为 null
// 场景3:对象合并(最常用)
setState({ count: 1 }) // 与现有状态智能合并
发布订阅的核心实现
js
listeners.forEach((listener) => listener(state, previousState));
这行代码是整个状态管理系统的神经中枢,负责将状态变化传播给所有关心的订阅者。
为什么传递两个参数?
js
listener(newState, oldState)
这种设计考虑了实际应用场景:
js
// 典型使用场景:状态对比分析
store.subscribe((newState, oldState) => {
// 用户切换检测
if (newState.user.id !== oldState.user.id) {
analytics.track('user_changed');
}
// 购物车变化检测
if (newState.cart.items.length > oldState.cart.items.length) {
showNotification('商品已添加到购物车');
}
});
❗️初始化函数
让我们来看这行看似简单但设计精妙的代码:
js
const initialState = (state = createState(setState, getState, api));
它等价于:
js
// 步骤1:执行用户函数并赋值给 state
state = createState(setState, getState, api);
// 步骤2:将赋值后的 state 保存为 initialState
const initialState = state;
理解执行流程:有无初始化函数的区别
这里的 createState
是用户传入的初始化函数,而不是 Zustand 内部定义的。为了更好地理解这个设计,让我们通过对比来看看它解决了什么问题。
方案A:不提供初始化函数(传统方式)
如果 Zustand 不要求传入 createState
,我们只能这样使用:
js
// 创建一个空的 store
const store = createStore();
// 手动设置初始状态
store.setState({ count: 0 });
// 每次操作都需要手动写状态更新逻辑
store.setState({ count: store.getState().count + 1 }); // 增加计数
store.setState({ count: store.getState().count - 1 }); // 减少计数
// 问题:
// 1. 业务逻辑分散,难以维护
// 2. 重复的状态更新代码
// 3. 容易出现闭包陷阱
// 4. 没有封装,每次都要手写逻辑
方案B:提供初始化函数
通过 createState
函数,我们可以优雅地定义状态和操作:
js
// 用户定义状态结构和操作逻辑
const store = createStore((set, get, api) => ({
// 🎯 声明式的初始状态
count: 0,
user: null,
// 🎯 封装好的操作方法,避免重复代码
increment: () => set(state => ({ count: state.count + 1 })),
decrement: () => set(state => ({ count: state.count - 1 })),
setUser: (user) => set({ user }),
// 🎯 计算属性和复杂逻辑
getDoubleCount: () => get().count * 2,
reset: () => set({ count: 0, user: null })
}));
// 使用变得简洁而直观
store.increment(); // 直接调用方法
store.decrement(); // 业务语义清晰
store.reset(); // 封装的复杂操作
为什么要这样设计?
这种设计模式叫做依赖注入:
- 控制反转:Zustand 把状态初始化的控制权交给用户
- 能力注入:用户的初始化函数可以使用 Zustand 提供的状态管理能力
- 闭包封装:用户定义的方法可以通过闭包访问到 set 和 get
框架适配层
Zustand 框架适配层的设计精妙,它充分展现了"分离关注点"的设计哲学。核心逻辑层专注于纯粹的状态管理,而框架适配层则负责将这些能力无缝集成到 React 生态中。
完整源码
js
// 打包后的 react.js
"use strict";
var React = require("react");
var vanilla = require("zustand/vanilla");
const identity = (arg) => arg;
function useStore(api, selector = identity) {
const slice = React.useSyncExternalStore(
api.subscribe,
() => selector(api.getState()),
() => selector(api.getInitialState())
);
return slice;
}
const createImpl = (createState) => {
const api = vanilla.createStore(createState);
const useBoundStore = (selector) => useStore(api, selector);
Object.assign(useBoundStore, api);
return useBoundStore;
};
const create = (createState) =>
createState ? createImpl(createState) : createImpl;
exports.create = create;
exports.useStore = useStore;
创建入口
js
const create = (createState) =>
createState ? createImpl(createState) : createImpl;
// 直接调用
const useStore = create((set) => ({ count: 0 }));
// 分步调用
const createStore = create();
const useStore = createStore((set) => ({ count: 0 }));
一个柯里化操作,用于初始化状态仓库,支持直接调用和分步调用两种方式。
createImpl:API 统一设计
js
const createImpl = (createState) => {
const api = vanilla.createStore(createState);
const useBoundStore = (selector) => useStore(api, selector);
Object.assign(useBoundStore, api);
return useBoundStore;
};
createImpl 是整个适配层的核心实现,它完成了三个关键任务:
- 调用核心层:通过
vanilla.createStore(createState)
创建核心逻辑层的仓库实例,获得完整的 API 对象 - 创建 Hook 函数:构建 useBoundStore 函数,它接受
selector
参数并调用useStore
获取状态切片 - API 能力扩展:使用
Object.assign
将原始仓库的所有方法暴露到 Hook 函数上
这里的关键在于理解 JavaScript 中函数本质上就是对象。这意味着我们可以像操作普通对象一样给函数添加属性和方法。
返回的 useBoundStore 既是一个 React Hook,又具备完整的仓库操作能力:
js
// 作为 Hook 使用,获取状态切片
function BearCounter() {
const bears = useBearStore((state) => state.bears);
return <h1>{bears} around here ...</h1>;
}
function Controls() {
const increasePopulation = useBearStore((state) => state.increasePopulation);
return <button onClick={increasePopulation}>one up</button>;
}
组件外操作能力
用户可以直接在组件外部操作全局状态:
js
useBoundStore.getState();
useBoundStore.setState({ ... });
useBoundStore.subscribe(() => {});
这种设计带来了强大的能力,可被用于事件处理与副作用管理等场景:
js
// 在事件处理器、API 调用、定时器等场景中直接操作状态
function handleGlobalEvent() {
const currentState = useBearStore.getState();
console.log('当前熊的数量:', currentState.bears);
// 直接更新状态,无需在组件内部
useBearStore.setState({ bears: currentState.bears + 10 });
}
// 全局状态监听与分析
useBearStore.subscribe((state, prevState) => {
if (state.bears !== prevState.bears) {
// 执行分析、日志记录等副作用
analytics.track('bears_count_changed', {
from: prevState.bears,
to: state.bears
});
}
});
// 异步操作中的状态管理
async function fetchAndUpdateBears() {
try {
const response = await fetch('/api/bears');
const data = await response.json();
useBearStore.setState({ bears: data.count });
} catch (error) {
console.error('获取熊数量失败:', error);
}
}
useStore:React 生态的完美桥梁
React 18 推出了一个专门为外部状态管理而生的 Hook: useSyncExternalStore
js
function useStore(api, selector = identity) {
const slice = React.useSyncExternalStore(
api.subscribe,
() => selector(api.getState()),
() => selector(api.getInitialState())
);
return slice;
}
参数
subscribe
:一个函数,接收一个单独的callback
参数并把它订阅到 store 上。当 store 发生改变时会调用提供的callback
,这将导致 React 重新调用getSnapshot
并在需要的时候重新渲染组件。subscribe
函数会返回清除订阅的函数。getSnapshot
:一个函数,返回组件需要的 store 中的数据快照。在 store 不变的情况下,重复调用getSnapshot
必须返回同一个值。如果 store 改变,并且返回值也不同了(用Object.is
比较,React 就会重新渲染组件。- 可选
getServerSnapshot
:一个函数,返回 store 中数据的初始快照。它只会在服务端渲染时,以及在客户端进行服务端渲染内容的激活时被用到。快照在服务端与客户端之间必须相同,它通常是从服务端序列化并传到客户端的。如果你忽略此参数,在服务端渲染这个组件会抛出一个错误。返回值
该 store 的当前快照,可以在你的渲染逻辑中使用。
这三个参数是否看起来很眼熟?它们正是核心逻辑层中 createStoreImpl
返回的 api 对象的核心属性!通过这样的参数传递,React 就能自动帮我们处理状态变化时的重新渲染逻辑。
中间件
中间件(Middleware)是 Zustand 实现功能扩展的核心机制,它允许我们在不修改核心代码的前提下,为状态管理注入额外的能力。官方 GitHub README 下面提到了两种中间件,我们分别来学习一下。
immer
如果你用过 React 开发真实项目,一定不会对 immer 陌生。在传统的状态管理中,我们必须严格遵循不可变更新原则:
js
// 🚫 危险:直接修改状态对象
setState(state => {
state.bees += 5; // 违反不可变性
return state;
});
// ✅ 正确但繁琐:手动创建新对象
setState(state => ({
...state,
bees: state.bees + 5
}));
对于深层嵌套的状态结构,这种写法既冗长又容易出错。Zustand 官方也考虑到了这一点,并为我们提供了两种集成 Immer 的方式:
方式二:直接使用 produce 函数
js
import { produce } from 'immer'
const useLushStore = create((set, get, store) => ({
lush: { forest: { contains: { a: 'bear' } } },
clearForest: () =>
set(
produce((state) => {
state.lush.forest.contains = null
}),
),
}))
const clearForest = useLushStore((state) => state.clearForest)
clearForest()
方式二:使用 immer 中间件(推荐)
js
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useBeeStore = create(
immer((set, get, store) => ({
bees: 0,
addBees: (by) =>
set((state) => {
state.bees += by
}),
})),
)
immer 中间件源码解析
getState = 获取当前状态
api = {setState, getState, subscribe} Core->>Immer: 调用 immer(initializer)(setState, getState, api) Note over Immer: 接收原始能力参数 Immer->>Immer: 增强 api.setState = immer版本 Note over Immer: 用 produce 包装函数式更新 Immer->>Original: 调用 initializer(增强的setState, getState, api) Note over Original: 用户代码接收到增强能力 Original->>Immer: 返回状态和方法 Immer->>Core: 返回初始状态 Core->>React: 返回完整 api React->>User: 返回 useBoundStore Hook
先贴一张详细的流程图,大家阅读代码困难的时候可以从图中找找灵感。
源码文件
js
// 打包后的 immer.js
'use strict';
var immer$1 = require('immer');
const immerImpl = (initializer) => (set, get, store) => {
store.setState = (updater, replace, ...args) => {
const nextState = typeof updater === "function" ? immer$1.produce(updater) : updater;
return set(nextState, replace, ...args);
};
return initializer(store.setState, get, store);
};
const immer = immerImpl;
exports.immer = immer;
这里的参数对应核心逻辑层中的关键调用,也是用户调用 create
时入参的函数的参数:
js
// 核心层 vanilla.js
const createStoreImpl = (createState) => {
const setState = (partial, replace) => { /* 原始状态更新逻辑 */ };
const getState = () => state;
const api = { setState, getState, getInitialState, subscribe };
// 🎯 关键调用:依赖注入
const initialState = (state = createState(setState, getState, api));
// ^^^^^^^^ ^^^^^^^^ ^^^
// 对应中间件的 set, get, store 参数
return api;
};
设计精髓:immer 中间件本质上是一个依赖注入容器,它接收核心层提供的原始能力,进行增强后再提供给用户。
类型判断
js
typeof updater === "function" ? immer$1.produce(updater) : updater
// 函数式更新:自动使用 immer 处理,支持"可变"语法
set((state) => {
state.bees += 10;
});
// 对象式更新:直接透传,保持原有行为
set({ bees: 5 });
函数包装与链式组合
js
const immerImpl = (initializer) => (set, get, store) => {
store.setState = (updater, replace, ...args) => {
// ↑ 包装函数:添加 immer 处理逻辑
const nextState = typeof updater === "function" ? produce(updater) : updater;
return set(nextState, replace, ...args); // 调用传入的 set 参数
// ^^^ 这里的 set 是核心层提供的原始 setState(单中间件时),或者是上一个中间件增强过的版本(多中间件时)
};
// 传递增强后的 setState 给下一层
return initializer(store.setState, get, store);
};
上面笔者画的那个流程图只考虑了一个中间件的情况,所以图中说的是调用原始的 setState
方法,但实际上如果是链式调用,调用的是上一个中间件增强过的 set
方法。
核心层只调用一次 createState
,而这个 createState
实际上是整个中间件链预组合的结果。也就是说,最终传递给 create
函数的初始化函数,已经是经过所有中间件层层包装后的产物。
persist
状态持久化是一个常见且重要的需求,比如用户不希望刷新页面后丢失购物车内容,也不希望重新登录后需要重新配置个人偏好。Zustand 提供的 persist 中间件的使用方式相当直观:
js
import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
const useFishStore = create(
persist(
(set, get) => ({
fishes: 0,
addAFish: () => set({ fishes: get().fishes + 1 }),
}),
{
name: 'food-storage', // name of the item in the storage (must be unique)
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
},
),
)
createJSONStorage 存储抽象层
createJSONStorage
是 persist
的核心依赖,我们先来介绍一下它:
js
function createJSONStorage(getStorage, options) {
let storage;
try {
storage = getStorage();
} catch (e) {
return;
}
const persistStorage = {
getItem: (name) => {
var _a;
const parse = (str2) => {
if (str2 === null) {
return null;
}
return JSON.parse(str2, options == null ? void 0 : options.reviver);
};
const str = (_a = storage.getItem(name)) != null ? _a : null;
if (str instanceof Promise) {
return str.then(parse);
}
return parse(str);
},
setItem: (name, newValue) => storage.setItem(name, JSON.stringify(newValue, options == null ? void 0 : options.replacer)),
removeItem: (name) => storage.removeItem(name)
};
return persistStorage;
}
该方法整体比较简单,就是通过包装函数增强了原生 storage 的能力,就不逐行解释了。它采用了适配器模式,具体作用作用如下:
- 统一接口:不管底层是 localStorage、sessionStorage、asyncStorage 还是自定义存储,都提供相同的 API
- 增强功能:在原生存储 API 基础上添加 JSON 序列化/反序列化能力
- 异步兼容:智能处理同步和异步存储,对上层透明
- 错误处理:优雅处理存储不可用的情况(
getStorage
不是直接传入存储对象,而是传入获取存储对象的函数,这样只会在调用的时候才报错,而不是页面执行的时候直接报错)
可选参数 options 中的 replacer 可以用于过滤敏感字段、自定义序列化逻辑(如 Date 对象处理),reviver 用于将其还原。
persist 源码解析
persist
的源码比较长,这里笔者提取了最核心的持久化存储逻辑代码,去除了水合、版本迁移等复杂逻辑,方便大家阅读学习:
js
const persistImpl = (config, baseOptions) => (set, get, api) => {
/*
* 这部分建立了 persist 中间件的配置体系。默认使用 localStorage 作为存储,partialize 函数默认保存完整状态,版本号从 0 开始。用户传入的 baseOptions 会覆盖这些默认值。
*/
let options = {
storage: createJSONStorage(() => localStorage),
partialize: (state) => state,
version: 0,
...baseOptions
};
let storage = options.storage;
/*
* 当存储不可用时(如浏览器隐私模式、存储配额满等),中间件不会阻塞应用运行,而是退化为普通的状态管理,只是失去了持久化能力。
*/
if (!storage) {
return config(
(...args) => {
console.warn(
`[zustand persist middleware] Unable to update item '${options.name}', the given storage is currently unavailable.`
);
set(...args);
},
get,
api
);
}
/*
* 这部分实现了持久化逻辑。
* 1. 获取当前状态
* 2. 使用 partialize 函数处理状态,只保存部分状态
* 3. 将状态和版本号打包存储
*/
const setItem = () => {
const currentState = { ...get() };
const stateToSave = options.partialize(currentState);
const dataToStore = {
state: stateToSave,
version: options.version
};
return storage.setItem(options.name, dataToStore);
};
/*
* 这部分实现了状态更新时的持久化逻辑。
* 1. 获取原始的 setState 函数
* 2. 重写 setState 函数,在更新状态后自动调用 setItem
*/
const originalSetState = api.setState;
api.setState = (state, replace) => {
originalSetState(state, replace);
void setItem();
};
const configResult = config(
(...args) => { // ← 这是增强版的 set 函数
set(...args); // ← 调用原始的 set(来自 vanilla 核心层)
void setItem(); // ← 自动保存到存储(void 确保不返回值)
},
get, // ← 原始的 get 函数
api // ← 原始的 api 对象
);
/*
* 这部分实现了 persist 对象,提供了清除存储、获取配置和设置配置的功能。
*/
api.persist = {
clearStorage: () => {
storage?.removeItem(options.name);
},
getOptions: () => options,
setOptions: (newOptions) => {
options = {
...options,
...newOptions
};
if (newOptions.storage) {
storage = newOptions.storage;
}
}
};
return configResult;
};
可以看出,persist
中间件的本质,就是通过代理模式拦截所有的状态更新操作,在状态发生变化时,自动向存储(如 localStorage)中保存一份状态快照。
总结
深入 Zustand 源码,看到一个优秀状态管理库的典范:用极简实现高效。相比 Redux 的 Action/Reducer 模板和 Provider 包裹,Zustand 直接修改状态、免 Provider 接入 React,并自动优化组件重渲染。从核心的发布订阅,到 React 的丝滑适配,再到中间件的灵活扩展,每一层都恰到好处。Zustand 的强大不在于复杂,而在于它用简洁优雅的方式解决了状态管理的核心问题------当你追求更少的样板、更直接的 API 和开箱即用的性能时,它就是上佳之选。
本来还想深入解析一下 useSyncExternalStore
的内部实现机制,但发现它涉及到 React 18 并发渲染、Fiber 架构等更深层的源码逻辑,这些内容足够单独写一篇文章了,就先在这里挖个坑,等我把 React 源码啃完再来填坑。