主题
第 14 章 · 状态管理 面试题集
Q1:什么时候需要全局状态管理?
考察点:状态管理的边界、过度设计避免
标准答案: 当满足以下任一条件时考虑:
- 状态在多个无父子关系的组件间共享(如登录用户信息、主题、通知)
- props 需要透传 3-5 层以上
- 跨页面/路由需要保持的状态(购物车、表单草稿)
- 状态变化要触发全局副作用(如全局 toast、推送通知)
- 复杂的状态机、多步流程
不需要的场景:
- 只在一个组件内用 →
useState/ref - 父子两层 → props
- 同级两个 → 提升到共同父级
通俗理解: 你家有一瓶可乐,全家都要喝 → 放冰箱(全局);只有你睡前嚼几粒糖 → 放卧室抽屉(组件 state)。
加分回答:
- 一句口诀:"props 传 5 层就该上了"
- 服务端状态(API 数据)可以考虑 React Query / SWR,区别于客户端状态库
- 不要"为了用而用",组件耦合度低 + 局部状态 + 上下文是更优解
追问:
- 服务端状态和客户端状态有什么区别?
- 全局状态太多会带来什么问题?
Q2:Redux 的核心三原则是什么?为什么要这样设计?
考察点:Redux 思想、可预测性
标准答案:
- 单一数据源(Single Source of Truth):整个应用 state 在一个 store 里,便于调试、序列化、持久化
- State 只读(State is Read-Only):唯一改变方式是 dispatch action,所有变化集中、可追踪
- 使用纯函数 reducer:
(state, action) => newState,相同输入相同输出,可测试、可时间旅行
通俗理解: Redux 像严格的银行系统——钱放总行金库(单一数据源),不能撬保险箱(只读),必须填取款单交柜员办理(reducer),每笔交易都记账可查(DevTools 时间旅行)。
代码示例:
js
function reducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INC': return { ...state, count: state.count + 1 };
default: return state;
}
}加分回答:
- 三原则保证了"可预测性 + 可调试性 + 可测试性"
- 时间旅行 / 撤销重做 / 中央日志都依赖纯函数 + 不可变
- Redux Toolkit 用 Immer 让你"看似可变写法",底层仍是不可变
追问:
- 为什么 reducer 不能有副作用?
- 时间旅行的实现原理是什么?
Q3:Redux Toolkit 解决了经典 Redux 的什么痛点?
考察点:Redux 演进、工程效率
标准答案: 经典 Redux 写法繁琐:
- 三处分散:action types / action creators / reducer
- 必须手写不可变更新(嵌套时大量
...spread) - 异步要装中间件(thunk / saga)
- 模板代码多到劝退新人
RTK 解决方案:
- createSlice:一个 slice 同时定义 reducer、action types、action creators
- 内置 Immer:可以"看似直接修改" state(底层仍不可变)
- configureStore:自动配好 devtools、thunk、序列化检查
- createAsyncThunk:标准化异步 action 流程
- RTK Query:内置数据请求层(缓存、轮询、失效)
代码示例:
js
const slice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
inc: (state) => { state.value++; }
}
});通俗理解: 经典 Redux 像"手写发票 + 三联单 + 复印 + 归档",RTK 像"扫码自动开票"——同一件事更少步骤更不易错。
加分回答:
- RTK 已是 Redux 官方推荐写法,不要再写经典 Redux 模板
- Immer 通过 Proxy 拦截"修改"操作,最终生成新对象
- RTK Query 类似 React Query,但与 Redux 整合更紧
追问:
- Immer 怎么实现"看似可变"?
- createAsyncThunk 自动 dispatch 哪几个 action?
Q4:Zustand 为什么这么火?相比 Redux 优势在哪?
考察点:状态库对比、API 设计
标准答案: Zustand 的优势:
- 极简 API:
create一行函数 + 当 Hook 用,无 Provider - 细粒度订阅:用 selector 只订阅需要的字段,避免无效重渲染
- TS 友好:类型推导自动
- 可在 React 外使用:
getState()/setState(),便于在事件 handler、工具函数中读写 - 轻:~1KB
- 支持中间件:persist、devtools、immer
- 没有 reducer/action 模板:直接写函数
劣势:
- 没有时间旅行(不如 Redux DevTools 强)
- 大型项目自由度太高,可能导致风格不统一
代码示例:
js
const useStore = create((set) => ({
count: 0,
inc: () => set(s => ({ count: s.count + 1 }))
}));
const count = useStore(s => s.count);通俗理解: Redux 是"严谨的银行",Zustand 是"墙上便签本"——大家都能看到,谁要更新就直接改写,没流程。
加分回答:
- 中小项目首选 Zustand,节省心智
- 状态切片可以拆多个 store(无 root reducer 限制)
- 可与 Immer 中间件配合,深层更新简化
追问:
- Zustand 没用 selector 会有什么问题?
- 怎么在 Zustand 里实现"持久化到 localStorage"?
Q5:Pinia 比 Vuex 好在哪?为什么 Vue 官方换了推荐?
考察点:Vue 状态管理演进
标准答案: Pinia 优势:
- 更简单的 API:去掉了 Vuex 的
mutations,直接在 action 里改 state - TS 支持极佳:类型自动推导
- 支持 Composition API:与 Vue 3 风格一致
- 多 store 扁平化:每个 store 独立,没有 module 嵌套套娃
- 更小:~6KB
- DevTools 体验更好
代码示例:
js
export const useCart = defineStore('cart', () => {
const items = ref([]);
const total = computed(() => items.value.reduce((s, i) => s + i.price, 0));
function add(item) { items.value.push(item); }
return { items, total, add };
});通俗理解: Vuex 像旧版 Redux(mutations + actions 双层),Pinia 像 RTK + Zustand 的合体——保留必要约定,去掉冗余。
加分回答:
- Pinia 已是 Vue 3 官方推荐,Vuex 进入维护模式
storeToRefs解决解构丢失响应性问题- 支持插件机制(pinia-plugin-persistedstate 等)
追问:
- Pinia 没有 mutations,怎么保证状态变化可追踪?
- 多个 store 之间如何通信?
Q6:你怎么选择 Redux / Zustand / Context?
考察点:技术选型能力
标准答案:
| 维度 | Context+useReducer | Zustand | Redux Toolkit |
|---|---|---|---|
| 项目规模 | 小 | 中小 | 中大 |
| 模板代码 | 少 | 极少 | 中 |
| DevTools | 弱 | 一般 | 强 |
| 中间件 | 自己写 | 有插件 | 完整生态 |
| 异步流程 | 自己写 | async action | createAsyncThunk |
| 团队规范 | 弱 | 弱 | 强 |
| 时间旅行 | 无 | 部分 | 完整 |
选择思路:
- 数据只跨 1-2 层 → 不用全局状态
- 共享几个简单字段(主题、登录态)→ Context + useReducer
- 全局状态多但不复杂、追求简洁 → Zustand
- 大型项目、多人协作、需要规范 → Redux Toolkit
通俗理解:
- Context = 公司便签栏(贴张纸大家看,但人多容易乱)
- Zustand = 共享 Excel 表(谁都能改,简单直接)
- Redux = ERP 系统(流程严格但复杂)
加分回答:
- 服务端数据用 React Query / SWR,不要塞进 Redux
- 状态库选型受团队习惯影响极大,统一比"最好"重要
追问:
- 你做过哪些状态库的迁移?踩过什么坑?
- 服务端状态和客户端状态分别用什么管理?
Q7:Redux 怎么处理异步?createAsyncThunk 是什么?
考察点:Redux 中间件、异步流程
标准答案: 经典 Redux 必须靠中间件处理异步:
- redux-thunk:action 可以是函数,函数内可异步
- redux-saga:基于 generator,复杂流程编排
- redux-observable:基于 RxJS
RTK 内置 createAsyncThunk,自动 dispatch 三个状态 action:
js
const fetchUser = createAsyncThunk('user/fetch', async (id) => {
const res = await fetch(`/api/user/${id}`);
return res.json();
});
// 自动 dispatch:
// fetchUser.pending → loading: true
// fetchUser.fulfilled → 收到数据
// fetchUser.rejected → 出错在 reducer 中用 extraReducers 监听:
js
extraReducers: (b) => {
b.addCase(fetchUser.pending, (s) => { s.loading = true; });
b.addCase(fetchUser.fulfilled, (s, a) => { s.loading = false; s.data = a.payload; });
b.addCase(fetchUser.rejected, (s, a) => { s.loading = false; s.error = a.error; });
}通俗理解: 异步 action 像"网购订单"——下单(pending)→ 发货(fulfilled)/ 退款(rejected),三个状态全自动通知。
加分回答:
- RTK Query 比 createAsyncThunk 更高级:自动缓存、重试、失效、轮询
- thunk 和 saga 的取舍:简单业务用 thunk,复杂工作流用 saga
- 异步 action 不污染 reducer 纯度(reducer 仍是纯函数,副作用在 thunk 里)
追问:
- redux-saga 的 generator 比 async/await 好在哪?
- RTK Query 和 React Query 有什么区别?
Q8:Redux 的 connect / useSelector 怎么避免无效重渲染?
考察点:性能优化、引用相等
标准答案:
useSelector默认用===比较返回值,引用变了就重渲染- 选择派生数据时容易"每次返回新对象" → 一直重渲染
优化手段:
多次小 selector:
jsxconst a = useSelector(s => s.x.a); const b = useSelector(s => s.x.b);传
shallowEqual:jsximport { shallowEqual } from 'react-redux'; const { a, b } = useSelector(s => s.x, shallowEqual);用 reselect 创建记忆化 selector:
jsconst selectFiltered = createSelector( [s => s.todos, s => s.filter], (todos, filter) => todos.filter(...) );
通俗理解: useSelector 像"小蜜蜂"——你告诉它要盯哪块蜜糖,它一变就告诉你。如果你每次都说"给我整罐蜜",那只要罐子换个标签它就以为你要重买(重渲染)。
加分回答:
- Zustand 自带浅比较 selector:
useStore(s => s.count)默认就是===比较 - React 18 的
useSyncExternalStore是底层订阅 hook,所有状态库都在用 - 避免在 selector 里每次
return list.filter(...),要 memoize
追问:
- reselect 和 useMemo 的区别?
- React-Redux 升级到 v8 后做了哪些性能优化?
Q9:状态库的"持久化"怎么做?
考察点:用户体验、跨页面状态
标准答案: "持久化"指页面刷新/关闭后,状态依然保留。常用方案是把指定 state 写入 localStorage / sessionStorage / IndexedDB。
各库的方案:
- Redux:
redux-persist,配置whitelist/blacklist决定哪些 reducer 持久化 - Zustand:内置
persist中间件 - Pinia:
pinia-plugin-persistedstate
js
// Zustand 示例
const useAuth = create(persist(
(set) => ({ token: null, setToken: (t) => set({ token: t }) }),
{ name: 'auth' }
));通俗理解: 持久化像"把临时账本誊写到主账本"——内存里的状态变了就同步抄到磁盘上,下次开机能再读回来。
加分回答:
- 不是所有 state 都要持久化:临时 UI 状态(开关、加载态)持久化反而是 bug 来源
- 大体积数据(如 Tree、缓存)放 IndexedDB,避免 localStorage 5MB 限制
- 安全敏感数据(token)应配合 expires 校验,避免历史 token 复用
追问:
- localStorage 和 sessionStorage 区别?
- 跨标签页同步状态怎么做?(
storage事件 / BroadcastChannel)
Q10:服务端状态和客户端状态的区别?
考察点:状态分类、数据请求新范式
标准答案:
- 客户端状态:UI 状态、用户操作产生的临时数据,只属于当前会话(如开关、表单输入、主题)
- 服务端状态:来自后端 API 的数据,有"远程真相",需要:
- 缓存与失效
- 重试与错误处理
- 后台轮询/订阅
- 乐观更新
- 多组件共享同一份数据
专用工具:React Query(TanStack Query)、SWR、RTK Query、Apollo(GraphQL)
js
const { data, isLoading } = useQuery({
queryKey: ['user', id],
queryFn: () => fetch(`/api/user/${id}`).then(r => r.json()),
staleTime: 60_000
});通俗理解:
- 客户端状态像"我家的垃圾"——我自己产生、自己清
- 服务端状态像"水电"——别人(服务器)供给,我用一份"本地缓存",定期同步真相
加分回答:
- 服务端状态塞进 Redux 是反模式(缓存策略、轮询、失效全自己写)
- React Query 缓存键基于
queryKey,变化自动重请求 - 现代应用:客户端状态用 Zustand,服务端状态用 React Query
追问:
- React Query 怎么实现"列表新增后自动刷新列表"?(invalidateQueries)
- 乐观更新(Optimistic Update)的流程是什么?
Q11:状态管理库的核心实现原理(订阅-发布)?
考察点:观察者模式、细粒度更新
标准答案: 所有状态库本质都是 订阅-发布模式:
- 状态存储:一个内部对象保存当前 state
- 订阅注册:组件挂载时通过
subscribe(listener)注册回调 - 状态更新:调用
setState/dispatch时更新内部 state - 通知订阅者:遍历 listener 数组,挨个调用,触发组件重渲染
- 细粒度优化:通过 selector 让 listener 只在"自己关心的字段变化"时触发
js
// 简化的 Zustand 实现
function create(initFn) {
let state;
const listeners = new Set();
const setState = (partial) => {
state = { ...state, ...(typeof partial === 'function' ? partial(state) : partial) };
listeners.forEach(l => l(state));
};
const getState = () => state;
const subscribe = (l) => { listeners.add(l); return () => listeners.delete(l); };
state = initFn(setState, getState);
return function useStore(selector = s => s) {
const [, force] = useReducer(c => c + 1, 0);
useEffect(() => subscribe(() => force()), []);
return selector(state);
};
}通俗理解: 就像微信群发消息——成员(组件)入群(subscribe),群主(store)发公告时所有人都收到通知,自己决定要不要做反应(重渲染)。
加分回答:
- React 18 新 hook
useSyncExternalStore提供官方订阅外部 store 的方式,避免 tearing 问题 - Pinia / Vue 响应式靠 Proxy 自动收集依赖,不用显式 subscribe
- 高级库(Jotai、Recoil)用"原子化"思想,每个原子独立订阅,更细粒度
追问:
- React 18 的 tearing 问题是什么?useSyncExternalStore 怎么解决?
- Jotai 的"原子"模型 vs Zustand 的"中央 store" 各有什么取舍?
Q12:Context API 算不算状态管理?什么时候不能用它替代 Redux?
考察点:API 边界、性能
标准答案: Context 是数据传递机制,不是完整的状态管理库——它只解决"跨层级传递",不解决"状态如何组织、如何更新、如何调试"。
搭配 useReducer,Context 可作为简易状态管理使用,但有几个不能替代 Redux 的硬伤:
- 无中央 store:多个 Context 嵌套层叠,不便集中管理
- 无中间件:日志、持久化、异步流程要自己写
- 无 DevTools:调试不便
- 重渲染粒度粗:value 变化时所有消费者重渲染,无 selector
- 不擅长高频更新:动画、鼠标位置等会引发性能问题
适合 Context 的场景:
- 主题、Locale、用户身份等低频变化的全局值
- 依赖注入(service 实例)
- 局部小型 store(局部 Provider)
通俗理解: Context 像办公室的广播喇叭——传话能力可以,但谁都听到(重渲染),且没录音回放。Redux 像群聊 + 历史记录 + @某人功能。
加分回答:
- Context 频繁更新可拆成多 Context(高频/低频分开)
- 把 value 用 useMemo 稳定,避免父组件无关 state 变化触发 Provider 更新
- 长远看,"Context + useReducer" 在中等场景仍很有竞争力(无第三方依赖)
追问:
- 怎么用 Context + useReducer 实现一个 mini Redux?
- 多个 Provider 嵌套时性能如何?