滚动截屏宝
120.38M · 2026-03-07
同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
很多同学会问:Vue 组件里已经有 data 了,为什么还要 Vuex 或 Pinia?
简单说:
data 是组件私有的,只能在当前组件和子组件间传递适用场景:
下面结合真实项目里常见的坑,从循环依赖、状态污染、调试技巧三个方面说明。
A 依赖 B,B 又依赖 A,形成闭环,就是循环依赖。
在状态管理里常见两种:
userStore 的 action 调 cartStore,cartStore 又调回 userStorestoreA 引用 storeB,storeB 引用 storeA后果:
undefined// 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 的复杂调用链,就容易形成逻辑上的“互相依赖”。
原则:依赖关系尽量是单向的,公共逻辑放到独立的 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 依赖清晰undefineduseXxxStore() 的引用,检查是否形成闭环“状态污染”一般指:
常见表现:
// 错误:在组件里直接改 store
<template>
<button @click="userStore.user.name = '张三'">改名</button>
</template>
<script setup>
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
</script>
问题:
正确做法:所有修改都通过 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>
这是最容易踩的坑:把 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.listtodoStore.toggleTodo(id) 等 action| 场景 | 错误写法 | 正确思路 |
|---|---|---|
| 修改 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 回写 |
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)
}
$subscribe 观察变化(Pinia)// 在 main.js 或 store 初始化处
const userStore = useUserStore()
userStore.$subscribe((mutation, state) => {
console.log('[UserStore] 变化', mutation.type, state)
})
适合排查“谁在什么时候改了这个 state”。
复杂场景下,可以在关键节点打印 state 的深拷贝,对比前后差异:
import { cloneDeep } from 'lodash-es'
const before = cloneDeep(store.$state)
// ... 执行某些操作 ...
const after = cloneDeep(store.$state)
console.log('diff', diff(before, after))
开发时可以在心里过一遍:
循环依赖
状态污染
store.xxx调试
$subscribe 或状态快照状态管理本身不难,难的是在团队协作和长期维护下保持结构清晰、可追踪。
先避免循环依赖和状态污染,再配合好用的调试方式,大部分问题都能快速定位。
后面有时间,可以再专门写一写 Vuex 和 Pinia 的对比、迁移,以及大型项目里的模块划分方式。
一、《Vue状态管理扫盲篇:Vuex 到 Pinia | 为什么大家都在迁移?核心用法对比》
二、《Vue状态管理扫盲篇:如何设计一个合理的全局状态树 | 用户、权限、字典、布局配置》
三、《Vue状态管理扫盲篇:在组件中优雅地使用 Pinia | 类型提示、解构、持久化》
四、《Vue状态管理扫盲篇:状态管理中的常见坑 | 循环依赖、状态污染与调试技巧》
跟着系列慢慢学,把技术功底扎扎实实地打牢~
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~