Skip to content

第 14 章 · 状态管理 面试题集

Q1:什么时候需要全局状态管理?

考察点:状态管理的边界、过度设计避免

标准答案: 当满足以下任一条件时考虑:

  1. 状态在多个无父子关系的组件间共享(如登录用户信息、主题、通知)
  2. props 需要透传 3-5 层以上
  3. 跨页面/路由需要保持的状态(购物车、表单草稿)
  4. 状态变化要触发全局副作用(如全局 toast、推送通知)
  5. 复杂的状态机、多步流程

不需要的场景:

  • 只在一个组件内用 → useState / ref
  • 父子两层 → props
  • 同级两个 → 提升到共同父级

通俗理解: 你家有一瓶可乐,全家都要喝 → 放冰箱(全局);只有你睡前嚼几粒糖 → 放卧室抽屉(组件 state)。

加分回答

  • 一句口诀:"props 传 5 层就该上了"
  • 服务端状态(API 数据)可以考虑 React Query / SWR,区别于客户端状态库
  • 不要"为了用而用",组件耦合度低 + 局部状态 + 上下文是更优解

追问

  • 服务端状态和客户端状态有什么区别?
  • 全局状态太多会带来什么问题?

Q2:Redux 的核心三原则是什么?为什么要这样设计?

考察点:Redux 思想、可预测性

标准答案

  1. 单一数据源(Single Source of Truth):整个应用 state 在一个 store 里,便于调试、序列化、持久化
  2. State 只读(State is Read-Only):唯一改变方式是 dispatch action,所有变化集中、可追踪
  3. 使用纯函数 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 解决方案:

  1. createSlice:一个 slice 同时定义 reducer、action types、action creators
  2. 内置 Immer:可以"看似直接修改" state(底层仍不可变)
  3. configureStore:自动配好 devtools、thunk、序列化检查
  4. createAsyncThunk:标准化异步 action 流程
  5. 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 的优势:

  1. 极简 APIcreate 一行函数 + 当 Hook 用,无 Provider
  2. 细粒度订阅:用 selector 只订阅需要的字段,避免无效重渲染
  3. TS 友好:类型推导自动
  4. 可在 React 外使用getState() / setState(),便于在事件 handler、工具函数中读写
  5. :~1KB
  6. 支持中间件:persist、devtools、immer
  7. 没有 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 优势:

  1. 更简单的 API:去掉了 Vuex 的 mutations,直接在 action 里改 state
  2. TS 支持极佳:类型自动推导
  3. 支持 Composition API:与 Vue 3 风格一致
  4. 多 store 扁平化:每个 store 独立,没有 module 嵌套套娃
  5. 更小:~6KB
  6. 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+useReducerZustandRedux Toolkit
项目规模中小中大
模板代码极少
DevTools一般
中间件自己写有插件完整生态
异步流程自己写async actioncreateAsyncThunk
团队规范
时间旅行部分完整

选择思路

  1. 数据只跨 1-2 层 → 不用全局状态
  2. 共享几个简单字段(主题、登录态)→ Context + useReducer
  3. 全局状态多但不复杂、追求简洁 → Zustand
  4. 大型项目、多人协作、需要规范 → 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 默认用 === 比较返回值,引用变了就重渲染
  • 选择派生数据时容易"每次返回新对象" → 一直重渲染

优化手段

  1. 多次小 selector

    jsx
    const a = useSelector(s => s.x.a);
    const b = useSelector(s => s.x.b);
  2. shallowEqual

    jsx
    import { shallowEqual } from 'react-redux';
    const { a, b } = useSelector(s => s.x, shallowEqual);
  3. 用 reselect 创建记忆化 selector

    js
    const 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

各库的方案:

  • Reduxredux-persist,配置 whitelist/blacklist 决定哪些 reducer 持久化
  • Zustand:内置 persist 中间件
  • Piniapinia-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:状态管理库的核心实现原理(订阅-发布)?

考察点:观察者模式、细粒度更新

标准答案: 所有状态库本质都是 订阅-发布模式

  1. 状态存储:一个内部对象保存当前 state
  2. 订阅注册:组件挂载时通过 subscribe(listener) 注册回调
  3. 状态更新:调用 setState/dispatch 时更新内部 state
  4. 通知订阅者:遍历 listener 数组,挨个调用,触发组件重渲染
  5. 细粒度优化:通过 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 的硬伤:

  1. 无中央 store:多个 Context 嵌套层叠,不便集中管理
  2. 无中间件:日志、持久化、异步流程要自己写
  3. 无 DevTools:调试不便
  4. 重渲染粒度粗:value 变化时所有消费者重渲染,无 selector
  5. 不擅长高频更新:动画、鼠标位置等会引发性能问题

适合 Context 的场景

  • 主题、Locale、用户身份等低频变化的全局值
  • 依赖注入(service 实例)
  • 局部小型 store(局部 Provider)

通俗理解: Context 像办公室的广播喇叭——传话能力可以,但谁都听到(重渲染),且没录音回放。Redux 像群聊 + 历史记录 + @某人功能。

加分回答

  • Context 频繁更新可拆成多 Context(高频/低频分开)
  • 把 value 用 useMemo 稳定,避免父组件无关 state 变化触发 Provider 更新
  • 长远看,"Context + useReducer" 在中等场景仍很有竞争力(无第三方依赖)

追问

  • 怎么用 Context + useReducer 实现一个 mini Redux?
  • 多个 Provider 嵌套时性能如何?