同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
一、开篇:状态管理在解决什么问题?
很多同学会问:Vue 组件里已经有 data 了,为什么还要 Vuex 或 Pinia?
简单说:
data是组件私有的,只能在当前组件和子组件间传递- 跨组件、跨页面共享的状态,用 props 一层层传会变成“面条代码”,难维护
- 状态管理库(Vuex / Pinia)提供集中式、可预测、可追踪的状态,更适合复杂应用
适用场景:
- 用户信息、权限
- 购物车、订单
- 全局 UI 状态(主题、侧边栏等)
- 多个模块都要读写的公共数据
下面结合真实项目里常见的坑,从循环依赖、状态污染、调试技巧三个方面说明。
二、坑一:循环依赖——模块互相“指回来”了
1. 什么是循环依赖?
A 依赖 B,B 又依赖 A,形成闭环,就是循环依赖。
在状态管理里常见两种:
- actions 互相调用:
userStore的 action 调cartStore,cartStore又调回userStore - store 模块互相引用:
storeA引用storeB,storeB引用storeA
后果:
- 加载顺序不确定,可能出现
undefined - 运行时死循环或逻辑错乱
- 难以调试和定位问题
2. 错误示例:actions 互相调用(Pinia 写法)
// store/user.js
import { defineStore } from 'pinia'
import { useCartStore } from './cart'
export const useUserStore = defineStore('user', {
state: () => ({ user: null }),
actions: {
async login(credentials) {
const res = await api.login(credentials)
this.user = res.data
// 登录成功后,同步购物车
const cartStore = useCartStore() // ⚠️ 问题开始
await cartStore.syncCart()
}
}
})
// store/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
actions: {
async syncCart() {
const userStore = useUserStore() // ⚠️ 又引用了 user
if (!userStore.user) return
// 根据 user 拉取购物车...
}
}
})
这里如果再有其他地方触发 userStore.login 和 cartStore.syncCart 的复杂调用链,就容易形成逻辑上的“互相依赖”。
3. 正确思路:单向依赖 + 抽离公共逻辑
原则:依赖关系尽量是单向的,公共逻辑放到独立的 service 层或顶层 action。
示例重构:
// services/cartSync.js - 抽离成独立服务
import { api } from '@/api'
export async function syncUserCart(userId) {
const res = await api.getCart(userId)
return res.data
}
// store/user.js - 只负责 user
export const useUserStore = defineStore('user', {
state: () => ({ user: null }),
actions: {
async login(credentials) {
const res = await api.login(credentials)
this.user = res.data
return this.user // 只返回用户信息,不直接调 cart
}
}
})
// store/cart.js - 依赖 user 的数据,但不反向调用 user
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
actions: {
async syncCart(userId) {
const items = await syncUserCart(userId) // 用 service,不引用 userStore
this.items = items
}
}
})
// 在组件或页面里统一编排流程
async function onLogin() {
await userStore.login(form)
if (userStore.user) {
await cartStore.syncCart(userStore.user.id)
}
}
这样:
user→cart依赖清晰- 不再有 actions 之间的循环调用
4. 如何排查循环依赖?
- 看报错堆栈:是否有“循环引用”或
undefined - 画一张 store/action 调用关系图
- 搜索
useXxxStore()的引用,检查是否形成闭环
三、坑二:状态污染——别直接改 store 里的引用类型
1. 什么是状态污染?
“状态污染”一般指:
- 不通过官方提供的 mutation/action 修改 state
- 直接修改 state 里的引用类型(对象、数组),导致“意料之外的共享修改”
常见表现:
- 修改一个组件里的“副本”,却影响了别的组件
- 回退、撤销时状态不对
- 和 Vue 的响应式、时间旅行调试冲突
2. 错误示例一:直接修改 state
// ❌ 错误:在组件里直接改 store
<template>
<button @click="userStore.user.name = '张三'">改名</button>
</template>
<script setup>
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
</script>
问题:
- 绕过 action,无法做校验、副作用、日志
- 不利于维护和团队协作
正确做法:所有修改都通过 action
// store/user.js
actions: {
updateName(name) {
if (!name || name.length < 2) {
throw new Error('昵称至少 2 个字符')
}
this.user = { ...this.user, name }
}
}
// 组件
<button @click="userStore.updateName('张三')">改名</button>
3. 错误示例二:引用类型“浅拷贝”导致共享修改
这是最容易踩的坑:把 store 里的对象/数组直接赋给局部变量,再改这个变量,其实改的是同一块内存。
// store/todo.js
state: () => ({
list: [
{ id: 1, text: '买菜', done: false },
{ id: 2, text: '做饭', done: false }
]
})
// 组件里
const todoStore = useTodoStore()
// ❌ 错误:把引用直接给了局部变量
const item = todoStore.list.find(t => t.id === 1)
item.done = true // 改的是 store 里的原对象!
// 或者
const list = todoStore.list
list.push({ id: 3, text: '洗碗', done: false }) // 直接改 store 的数组
问题:
- 看起来像是“副本”,其实是引用
- 多个地方共享同一数据,一处改处处改,难以排查
正确做法:需要修改时,用新对象/新数组替换
// store/todo.js
actions: {
toggleTodo(id) {
const index = this.list.findIndex(t => t.id === id)
if (index === -1) return
const item = { ...this.list[index], done: !this.list[index].done }
this.list = [
...this.list.slice(0, index),
item,
...this.list.slice(index + 1)
]
},
addTodo(todo) {
this.list = [...this.list, { ...todo, id: Date.now() }]
}
}
在组件里:
- 只读:直接用
todoStore.list - 要改:只调用
todoStore.toggleTodo(id)等 action
4. 表格小结
| 场景 | 错误写法 | 正确思路 |
|---|---|---|
| 修改 state | store.xxx = value | 通过 action 修改 |
| 修改对象属性 | const obj = store.obj 后 obj.a = 1 | 在 action 里 store.obj = { ...obj, a: 1 } |
| 修改数组 | store.arr.push(x) | store.arr = [...store.arr, x] |
| 传给子组件“可编辑副本” | 直接传 store.xxx 再在子组件里改 | 传副本,改完通过事件/action 回写 |
四、调试技巧:快速定位状态问题
1. Vue DevTools 中看 Pinia / Vuex
- 安装 Vue DevTools 浏览器扩展
- 打开应用后,在 DevTools 里找到 Vue 面板
- 点 Pinia 或 Vuex,可以:
- 查看每个 store 的 state
- 看每个 mutation/action 的调用记录
- 对 state 做简单的手动修改(调试用)
2. 在关键 action 里加日志
actions: {
async fetchUser() {
console.group('[UserStore] fetchUser')
try {
const res = await api.getUser()
this.user = res.data
console.log('success', this.user)
} catch (e) {
console.error('failed', e)
}
console.groupEnd()
}
}
生产环境用环境变量包一层,避免日志泄露:
if (import.meta.env.DEV) {
console.log('[UserStore] state after action', this.$state)
}
3. 用 $subscribe 观察变化(Pinia)
// 在 main.js 或 store 初始化处
const userStore = useUserStore()
userStore.$subscribe((mutation, state) => {
console.log('[UserStore] 变化', mutation.type, state)
})
适合排查“谁在什么时候改了这个 state”。
4. 时间旅行调试(概念)
- Vue DevTools 支持录制、回放 state 变化
- 可跳到某一时刻的 state,看当时的界面表现
- 有利于重现“偶尔才出现”的 bug
5. 状态“快照”对比
复杂场景下,可以在关键节点打印 state 的深拷贝,对比前后差异:
import { cloneDeep } from 'lodash-es'
const before = cloneDeep(store.$state)
// ... 执行某些操作 ...
const after = cloneDeep(store.$state)
console.log('diff', diff(before, after))
五、总结:一份可对照的检查清单
开发时可以在心里过一遍:
-
循环依赖
- 没有 store 之间的循环引用
- actions 之间的调用是单向的
- 公共逻辑抽到 service 或顶层
-
状态污染
- 不在组件里直接改
store.xxx - 修改对象/数组时用新引用,不直接改原引用
- 所有修改都通过 action 完成
- 不在组件里直接改
-
调试
- 会用 Vue DevTools 看 Pinia/Vuex
- 关键 action 有必要的日志(开发环境)
- 复杂问题会考虑
$subscribe或状态快照
六、结语
状态管理本身不难,难的是在团队协作和长期维护下保持结构清晰、可追踪。
先避免循环依赖和状态污染,再配合好用的调试方式,大部分问题都能快速定位。
后面有时间,可以再专门写一写 Vuex 和 Pinia 的对比、迁移,以及大型项目里的模块划分方式。
🔍 本系列专栏导航
一、《Vue状态管理扫盲篇:Vuex 到 Pinia | 为什么大家都在迁移?核心用法对比》
二、《Vue状态管理扫盲篇:如何设计一个合理的全局状态树 | 用户、权限、字典、布局配置》
三、《Vue状态管理扫盲篇:在组件中优雅地使用 Pinia | 类型提示、解构、持久化》
四、《Vue状态管理扫盲篇:状态管理中的常见坑 | 循环依赖、状态污染与调试技巧》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~