🔥 面试必杀技:手写 Zustand,彻底搞懂 React 状态管理的"中央银行"模式
导读 :在 React 面试中,被问到"如何实现一个轻量级状态管理库"时,90% 的候选人会背诵 Redux 流程或 Context API 的缺点。但如果你能现场手写一个 Zustand 的核心逻辑,并用"中央银行"的比喻清晰阐述其发布订阅 与按需更新的原理,你将直接锁定 Offer。本文将以掘金深度解析的风格,带你从零构建一个迷你版 Zustand。
🏦 一、背景:为什么我们需要"中央银行"?
在 React 的组件王国里,状态管理经历了三个时代的演变:
-
Redux 时代(繁琐的 bureaucracy):
- 修改一个状态需要:
Dispatch Action->Reducer->Store Update。 - 痛点:样板代码太多,像去银行办事要填无数张单子。
- 修改一个状态需要:
-
Context API 时代(大喇叭广播):
- 虽然去掉了 Redux 的繁琐,但引入了新问题:上下文穿透。
- 痛点 :一旦
Provider的值变化,所有 消费该 Context 的子组件,无论是否用到变化的数据,都会无条件重渲染。这就像央行发通知,不管你是关心利率还是汇率,全村人都得跑出来听一遍。
-
Zustand 时代(精准的中央银行):
- 无围墙 :不需要
<Provider>包裹,组件随时随地访问。 - 精准广播 :基于发布订阅模式 (Pub/Sub)。组件只订阅自己关心的数据切片(Slice)。
- 直接操作 :直接在组件内调用
set修改状态,无需 dispatch。
- 无围墙 :不需要
🛠️ 二、核心实现:构建"金库" (Vanilla JS Store)
Zustand 的灵魂在于它完全独立于 React。我们先剥离框架,用原生 JS 实现一个支持发布订阅 和不可变更新的 Store。
💻 代码实现:createStore
javascript
/**
* 核心工厂:createStore
* 模拟 Zustand 底层的 createStoreImpl
*/
export function createStore(createState) {
// 1. 【金库账本】(State)
// 使用闭包变量存储状态,外部无法直接修改,保证安全性
let state;
// 2. 【听众名单】(Listeners)
// 使用 Set 数据结构。
// 面试题点:为什么用 Set 不用 Array?
// 答:Set 自动去重,且删除操作 (delete) 的时间复杂度为 O(1),适合高频增删的订阅场景。
const listeners = new Set();
// 3. 【获取账本】(Get)
const getState = () => state;
// 4. 【核心:修改账本 & 广播】(Set)
const setState = (partial) => {
// A. 计算新状态 (支持函数式更新和对象式更新)
// 对应源码逻辑:如果是函数则执行,否则直接使用
const nextState = typeof partial === 'function' ? partial(state) : partial;
// B. 【不可变性关键】浅合并 (Shallow Merge)
// 确保每次更新都返回一个新的对象引用,这是 React 检测变化的基础
state = typeof nextState === 'object' && nextState !== null
? { ...state, ...nextState }
: nextState;
// C. 【广播时刻!】(Publish)
// 遍历所有监听器,通知它们"数据变了",并传入最新状态
// 注意:实际生产中这里可能需要处理异步队列,但核心逻辑是 forEach
listeners.forEach((listener) => listener(state));
};
// 5. 【办理订阅】(Subscribe)
const subscribe = (listener) => {
// 将监听函数加入名单
listeners.add(listener);
// 【重要考点】返回取消订阅函数 (Unsubscribe)
// 作用:防止内存泄漏。当组件卸载时,必须调用此函数将 listener 从 Set 中移除。
return () => {
listeners.delete(listener);
};
};
// 6. 【初始化金库】
// 调用用户传入的 createState 函数,注入 set 和 get 工具
// 此时 state 被正式赋值(例如:{ count: 0, increment: fn })
state = createState(setState, getState);
// 7. 【交付接口】
return {
getState,
setState,
subscribe,
// 实际源码还有 destroy 等方法,面试手写这三个核心即可
};
}
📝 掘金式知识点植入
- 闭包的力量 :
state和listeners被封闭在createStore的作用域内,形成了真正的私有变量,外界只能通过暴露的方法访问。 - 中间件原理 :Zustand 的强大中间件(如
persist,devtools)本质上是高阶函数 。它们包裹setState和getState,在数据读写前后插入自定义逻辑(如写入 localStorage)。
⚛️ 三、桥梁搭建:连接 React (Hook 封装)
光有金库不行,React 组件需要在数据变化时重新渲染 。我们需要编写一个自定义 Hook useStore。
💻 代码实现:经典 Hooks 版 (原理演示)
为了展示对 React 生命周期的理解,我们先使用 useState + useEffect 实现。
javascript
import { useState, useEffect } from 'react';
/**
* 工厂函数:create
* 接收 createState,返回一个可以在组件中使用的 Hook (useStore)
*/
export function create(createStateFn) {
// 1. 实例化唯一的"金库" (Store)
// 注意:这个 store 在模块作用域内是单例的,所有组件共享这一个实例
const store = createStore(createStateFn);
// 2. 返回自定义 Hook
// selector: 选择器函数,用于"按需订阅" (Zustand 的核心优势)
// equalityFn: 可选的相等性判断函数 (用于性能优化)
return function useStore(selector, equalityFn) {
// 默认选择器:返回整个 state
const sel = selector || ((s) => s);
// A. 【本地状态】
// 用于触发当前组件的重渲染。
// 初始化时,立即同步获取一次最新数据
const [slice, setSlice] = useState(() => sel(store.getState()));
// B. 【副作用:订阅与清理】
useEffect(() => {
// 定义回调函数:当 store 变化时执行
const onUpdate = () => {
const nextSlice = sel(store.getState());
// 【性能优化关键点】
// 如果提供了相等性判断函数,且数据没变,则不触发 setState
// 避免无效重渲染 (Zustand 默认行为通常包含浅比较)
if (equalityFn && equalityFn(slice, nextSlice)) {
return;
}
// 数据确实变了,更新本地状态,触发 React 重渲染
setSlice(nextSlice);
};
// 1. 订阅金库
const unsubscribe = store.subscribe(onUpdate);
// 2. 再次检查 (Double Check)
// 防止在 useEffect 执行前,store 已经发生了变化导致的丢失更新
onUpdate();
// 3. 清理函数 (组件卸载时)
// 调用 unsubscribe,从 Set 中移除 listener,防止内存泄漏
return unsubscribe;
}, [store, sel, equalityFn]);
return slice;
};
}
🚀 面试加分项:口述 useSyncExternalStore
写完上述代码后,务必补充以下内容,体现你对 React 18 新特性的掌握:
"面试官,上面的写法是经典的
useState+useEffect模式,能够很好地解释原理。但在 React 18 之后,Zustand 源码已经升级使用useSyncExternalStoreAPI。为什么要用它?
- 防止 Tearing (数据撕裂):在并发渲染 (Concurrent Mode) 下,旧写法可能导致 UI 显示不一致的状态快照。
- SSR 友好:更好地支持服务端渲染的数据注水。
如果用
useSyncExternalStore改写,核心逻辑会变得极其简洁:
javascriptimport { useSyncExternalStore } from 'react'; return function useStore(selector) { return useSyncExternalStore( store.subscribe, // 订阅函数 () => selector(store.getState()), // getSnapshot: 获取当前客户端快照 () => selector(store.getState()) // getServerSnapshot: 获取服务端快照 ); };这样代码更简洁,且由 React 内部保证调度一致性。"
🧩 四、实战场景:为什么它比 Context 快?
让我们用刚才手写的库,模拟一个经典场景,展示 Selector (选择器) 的威力。
场景:用户信息更新
假设我们有一个包含 name 和 age 的状态。
javascript
const useUserStore = create((set) => ({
name: 'Alice',
age: 25,
updateName: (name) => set({ name }),
updateAge: (age) => set({ age })
}));
// 组件 A:只关心名字
function NameDisplay() {
// 🔥 关键点:只订阅 name
// 即使 age 改变了,这个组件也不会重渲染!
const name = useUserStore((state) => state.name);
console.log('NameDisplay Rendered');
return <h2>Hello, {name}</h2>;
}
// 组件 B:只关心年龄
function AgeDisplay() {
// 🔥 关键点:只订阅 age
// 即使 name 改变了,这个组件也不会重渲染!
const age = useUserStore((state) => state.age);
console.log('AgeDisplay Rendered');
return <p>Age: {age}</p>;
}
对比 Context API : 如果使用 Context,当 updateName 触发时,<UserContext.Provider> 的值改变,NameDisplay 和 AgeDisplay 都会重渲染,哪怕 AgeDisplay 根本没用到了 name。这就是 Zustand "精准广播"的优势。
🗺️ 五、面试思维导图 (Memory Map)
在白板面试中,你可以按照这个结构进行陈述:
💡 六、总结与回答策略
如果在面试中被问到"请手写一个状态管理库"或"讲讲 Zustand 原理",请按以下步骤输出:
- 开场定调 :一句话概括,"Zustand 本质是一个基于发布订阅模式的状态容器,通过闭包维护状态,利用 React Hooks 实现视图联动,解决了 Context 的全量重渲染问题。"
- 手写 Store :重点写出
Set存监听器、setState里的遍历通知、以及subscribe返回取消函数。一定要提到浅合并保证不可变性。 - 手写 Hook :重点写出
useEffect中如何订阅,以及如何通过selector提取状态存入useState来驱动渲染。 - 展示深度 :主动提及
useSyncExternalStore解决并发问题,以及中间件是如何通过装饰器模式增强set/get的。 - 结合场景 :用上面的
NameDisplay/AgeDisplay例子说明"为什么需要 Selector",这是 Zustand 优于 Context 的关键点。
掌握这套逻辑,你不仅学会了 Zustand,更深刻理解了响应式系统的设计精髓。