← 全部材料 状态管理
STATE · 10 个高频考点

状态不是存哪都行

状态管理题的失分点很少是"不会用 API",而是说不清为什么这么设计:mutation 为什么必须同步、reducer 为什么必须返回新对象、Context 为什么不能替代它。这份逐条讲清代价。

01先看通知机制三个库的差别全在"怎么知道该更新谁",抓住这点其他都是派生。
02认准唯一真相源同一份数据存两处,迟早不一致。能算出来的不要存。
03分层再选型组件局部、页面、全局、服务端四层,塞错层比用错库更麻烦。
1

全局状态管理到底在解决什么★

不是"组件之间传值"那么简单
组件通信只是表象。全局状态管理真正解决的是三件事:跨层级共享、状态变更可追踪、以及"什么时候该重新渲染"。第三件才是框架之间最大的差别——Vuex 靠响应式追踪,Redux 靠不可变 + 全量对比,Zustand 靠手动订阅。
◐ 可交互:改一个字段,看谁跟着重新渲染
store
count0
user—
themelight
组件 A
订阅 count
0 次渲染
组件 B
订阅 user
0 次渲染
组件 C
订阅整个 store
0 次渲染
组件 C 订阅了整个 store,所以任何字段变化它都会重渲染 —— 这就是没有 selector 的代价。

不用状态管理时的典型问题

  • props 层层透传,中间组件被迫接收无关数据
  • 兄弟组件通信要往上提到共同父级,父级变成"数据中转站"
  • 同一个数据在多处各存一份,改了一处忘了另一处
  • 出问题时不知道是谁改的,只能到处打断点

用状态管理之后

  • 任何组件直接读 store,不用透传
  • 写入集中在 action,可以打日志、加限制
  • 单一数据源,不会出现多份不一致
  • 每次变更都有记录,排查有据可依
面试怎么答全局状态管理不只解决跨层级传值,更重要是集中写入和变更可追踪。三个框架的差别在通知更新的机制上:Vuex 和 Pinia 靠响应式系统精确追踪依赖,Redux 靠不可变更新加 selector 比较,Zustand 靠手动订阅。Vue 里其实用 reactive 导出一个模块对象就能跨组件共享,小项目不一定需要 Pinia;React 里模块变量不会触发渲染,所以必须有订阅方案。
2

Vuex:那套约束为什么要存在

mutations 不是冗余设计
Vuex 的核心是四件套 + 一条纪律:state 存数据、getters 算派生、mutations 同步改、actions 跑异步。纪律是只能通过 mutation 改 state(开启严格模式后直接改会报错)。这条约束换来的是"每次变更都可被记录",devtools 的时间旅行就靠它。
面试怎么答Vuex 是 state 加 getters 算派生、mutations 同步改、actions 跑异步,一条纪律是只能通过 mutation 改 state。这条约束的意义在于让每次变更都能被 devtools 记录成快照,实现时间旅行,所以 mutation 必须是同步的。严格模式靠深度 watch 检测非法修改,只建议开发环境开。新项目官方推荐 Pinia,但老项目仍在 Vuex,所以还是会被问。
3

Pinia:为什么能砍掉 mutations★

Vuex 5 的形态提前来了
Pinia 的改动就一句话:action 既能同步也能异步,不再需要 mutation。它靠两层变化实现——每 store 一个独立响应式对象(没有单一 rootState)、以及 action 内部修改被 devtools 的 $onAction 拦截记录。约束放开了,记录能力没丢。
Pinia 与 Vuex 的四处关键差别1. mutations Pinia 没有,action 直接改 state 2. 模块化 Vuex 靠嵌套 modules + namespaced Pinia 每个 store 天然独立,互相 import 3. TypeScript Vuex 的类型推导很差,要手写 Pinia 的 setup 写法类型自动推导 4. 代码分割 Vuex 的 store 是单例,难以按需加载 Pinia 的 store 是函数,用到才初始化
面试怎么答Pinia 砍掉 mutations 是因为它每个 store 是独立的响应式对象,不需要单一 rootState 来统一提交;action 里改 state 之后,devtools 通过 $onAction 钩子记录下来,所以可追踪性没丢。相比 Vuex 还多了三大优势:模块天然独立不用 namespaced、TypeScript 类型能自动推导、store 是函数所以能按需加载。要注意解构 state 必须用 storeToRefs,否则丢响应式。
4

Redux:不可变和中间件★

为什么要返回新对象
Redux 唯一硬性要求是reducer 必须是纯函数并返回新对象。原因是它靠"引用变没变"来判断更新——同一个引用就跳过重渲染,所以不能原地改。所有副作用(请求、日志、路由跳转)都放到中间件里,因为 reducer 必须纯。
中间件的洋葱结构dispatch(action) │ ├─ middleware1 前半段 │ ├─ middleware2 前半段 │ │ ├─ reducer 执行 │ │ └─ middleware2 后半段 │ └─ middleware2 结束 └─ middleware1 后半段 所以中间件能: 在 reducer 前后各做一件事(日志、耗时统计) 决定要不要 next(action)(拦截) 改造 action 再传下去(thunk 就是这样)
面试怎么答Redux 要求 reducer 是纯函数返回新对象,因为它靠引用比较判断更新,原地改的话引用不变组件就不渲染。副作用不能放 reducer,所以请求、日志这些都放中间件,中间件的三层柯里化结构让它能在 reducer 前后各插一段逻辑,也能拦截或改造 action。现在标准写法是 Redux Toolkit,内部用 Immer 所以能写成像原地修改,实际仍生成新对象。
5

Zustand / Jotai:订阅模型★

React 状态库为什么越来越多
Zustand 的核心是一个 store 加一个 selector:组件通过 selector 声明"我只关心这块数据",只有它变了才重渲染。Jotai 更进一步,把状态拆成一个个原子,组件依赖哪个原子就订阅哪个,粒度天然更细。两者都不需要 Provider。
三种模型的更新粒度对比Redux:整棵树是一个对象 任何 action 都可能让 useSelector 重新执行 → 靠 selector 的返回值比较来过滤 → 写不好就整页重渲染 Zustand:整棵树也是一个对象,但订阅是逐组件的 selector 返回值变了才通知这个组件 → 比 Redux 少了 Provider 和 connect 的样板 Jotai / Recoil:状态拆成原子 组件只依赖用到的原子 → 一个原子变,只通知订阅它的组件 → 粒度最细,但原子多了要管理依赖图
面试怎么答Zustand 是一个 store 加 selector 的订阅模型,组件声明关心哪块数据,只有这块变了才重渲染,而且不需要 Provider。Jotai 把状态拆成原子,组件依赖哪个原子就订阅哪个,粒度更细。要注意 selector 返回新对象会导致无限重渲染,因为引用永远不相等,要拆开写或者用浅比较。React 生态状态库多,本质是因为 React 没有响应式系统,需要库来补精确更新这一块。
6

Context 不是状态管理★

它能传值,但不能高效更新
Context 解决的是透传问题,不是更新问题。value 一变,所有 useContext 的组件全部重渲染,没有 selector 也没有 bailout。所以正确用法是:Context 只放低频变化的值(主题、用户信息、i18n),高频状态交给状态库。
面试怎么答Context 解决的是跨层级传值,不是高效更新。它的 value 一变,所有消费组件都会重渲染,没有 selector 机制。所以正确做法是 Context 只放变化频率低的值,比如主题、当前用户、i18n、以及依赖注入用的 store 实例;高频变化的状态交给 Zustand 或 Redux 这类带订阅的库。如果一定要用 Context 装高频状态,至少要把稳定的 setState 和变化的值拆成两个 Provider。
7

状态该分几层★

不是所有数据都该进 store
按生命周期和共享范围分四层:组件局部状态、页面/路由状态、应用全局状态、服务端状态。塞错层是状态管理最常见的病——把服务端数据当全局状态存,就会出现"该刷新的时候不刷新、不该刷新的时候乱刷"。
四层各放什么【组件局部】useState / ref 输入框的临时值、hover 状态、展开收起 判据:只这一个组件用 【页面/路由】query 参数 或 页面级 store 筛选条件、分页、tab 判据:同一页面内多个组件用,换页面该丢 【应用全局】Pinia / Redux / Zustand 登录用户、权限、主题、全局配置 判据:跨页面存在,换页面不能丢 【服务端状态】React Query / SWR / vue-query 列表数据、详情数据 判据:数据源在服务端,需要缓存、失效、重试
面试怎么答状态按生命周期分四层。组件局部的用 useState 或 ref,只这一个组件用;页面级的筛选条件和分页放 URL 或页面级 store;跨页面的登录信息、权限、主题放全局 store;服务端数据单独一层,用 React Query 这类库管,因为它需要缓存、失效、重试、去重,塞进全局 store 会变成一堆样板代码。判断标准是唯一真相源,能算出来的不要存。
8

解构丢响应式:Vue 侧的高频坑

一个坑衍生出三个 API
Vue 的响应式靠 Proxy 拦截属性访问。一旦把属性值解构成普通变量,就绕过了代理,拿到的是当时的值,后续变化不会同步。Pinia 因此提供了 storeToRefs,Vue 3 因此需要 toRefs——这两个 API 存在的唯一理由就是这件事。
面试怎么答Vue 的响应式靠 Proxy 拦截属性访问,解构相当于读了一次值,脱离了代理,所以后续变化不同步。reactive 解构会丢,ref 不会,因为 ref 本身是对象。所以 Pinia 提供 storeToRefs 专门给 state 和 getters 用,action 是普通函数不用包。toRefs 只处理第一层,嵌套对象解构后还是会丢。React 没有这个问题,因为每次渲染都重新执行函数,但它有闭包陷阱。
9

持久化与跨标签同步

刷新丢状态怎么救
持久化就是写一份到 localStorage/sessionStorage,启动时读回来。跨标签页同步靠 storage 事件——注意它只在本标签之外触发,改自己的 localStorage 不会收到自己的事件。
面试怎么答持久化就是把需要的字段写进 localStorage,启动时读回来,Pinia 和 Zustand 都有现成插件,但一定要用白名单只存需要的字段,并且带版本号避免结构调整后读旧数据出错。跨标签同步靠 window 上的 storage 事件,要注意它只会在其他标签页触发,自己写不会收到自己的事件。另外 token 不建议只放 localStorage,因为它能被 XSS 读走,更稳的是 httpOnly cookie。
10

什么时候不该上全局状态

过度设计比没有设计更常见
三条明确的"不需要":只有一个组件用、可以从现有状态算出来、以及服务端数据。前两条用局部状态即可,第三条交给数据请求库。中小项目里,一个模块级 reactive / 一个自定义 hook 就够替代 Pinia。
替代方案,按复杂度递增1. 一个组件用 → useState / ref 2. 父子几层 → props(两三层内其实可接受) 3. 兄弟或跨两三层 → 状态提升到共同父级 4. 一个小模块内共享 → 模块级 reactive 对象 / 自定义 hook + Context 5. 跨多个页面、需追踪 → Pinia / Zustand 6. 服务端数据 → React Query / SWR / vue-query 判断依据不是"数据多不多",而是: 有多少个地方要读写?写的地方能不能收敛到一处?
面试怎么答是否需要全局状态,判断标准不是数据量大小,而是有多少地方读写、写操作能不能收敛到一处。一个组件用就局部状态,两三层父子用 props 或状态提升,一个小模块共享可以用模块级 reactive 或自定义 hook,跨页面且需要追踪才上 Pinia,服务端数据交给 React Query 这类库。要注意模块级 reactive 在 SSR 下会串数据,所以 SSR 项目必须用能按请求新建实例的方案。
11

速答清单

考前十分钟只看这一页
01全局状态管理真正解决什么?
跨层级共享、变更可追踪、以及精确控制"什么时候重渲染"。第三点是各框架差别最大的地方。
02Vuex 的 mutation 为什么必须是同步的?
devtools 在每次 mutation 后拍快照做时间旅行。有异步的话快照时机对不上,回放会错乱。
03严格模式有什么代价?
靠深度 watch 整个 state 检测非法修改,大对象上开销明显,所以只建议开发环境开。
04Pinia 为什么能去掉 mutations?
每个 store 是独立响应式对象,不需要统一提交;action 直接改 state,devtools 通过 $onAction 记录,可追踪性没丢。
05Pinia 解构为什么会丢响应式?
state 是 reactive 包的,解构等于读了一次值就脱离代理。要用 storeToRefs 转成 ref,action 是普通函数不用包。
06Redux 为什么必须返回新对象?
它靠引用比较判断更新,原地改引用不变就不会重渲染。无变化时返回原 state 是明确优化。
07Redux 中间件的三层柯里化结构?
store => next => action。让它在 reducer 前后各插一段逻辑,也能拦截或改造 action。
08RTK 里能写 state.push 为什么不算违反不可变?
内部用了 Immer,写法看起来像原地改,实际生成新的不可变对象。
09Zustand 的 selector 返回新对象会怎样?
无限重渲染。每次返回的对象引用都不一样。要拆成多次调用或者用 useShallow 做浅比较。
10Context 能替代状态管理吗?
不能。value 一变所有消费者全部重渲染,没有 selector。只适合放低频值:主题、用户信息、依赖注入的实例。
11状态应该怎么分层?
组件局部、页面/URL、应用全局、服务端数据四层。服务端数据用 React Query 这类库,不要塞进全局 store。
12为什么服务端数据不该放 store?
它需要缓存过期、并发去重、失败重试、焦点重取这些专属逻辑,手写进 store 就是几百行样板代码。
13模块级 reactive 在 SSR 里有什么问题?
整个应用生命周期只有一份,不同请求共享同一对象会串数据。SSR 必须用每请求新建实例的方案。
14跨标签页同步靠什么?
window 的 storage 事件。注意它只在本标签之外触发,自己写 localStorage 收不到自己的事件。
15token 放 localStorage 有什么风险?
XSS 能直接读走。更稳的是 httpOnly cookie 配合 CSRF 防护,localStorage 只放不敏感的偏好。
16中小项目可以不上状态库吗?
可以。模块级 reactive 或自定义 hook 加 Context 能覆盖大部分场景,但 SSR 项目和多人协作要考虑一致性。