外观
状态管理方案对比
是什么
「状态」= 会随时间变化、且界面要跟着变的数据(登录用户、购物车、表单草稿、弹窗开关……)。
状态管理的全部问题其实是:这个状态放在哪、谁能看到它、改它。放错位置的典型症状:props 一层层往下传五六层、两个不相干组件要同步一份数据、刷新后状态乱套。
四个层级(从小到大)
| 层级 | 方案 | 作用范围 | 适合放什么 | 成本 |
|---|---|---|---|---|
| 1 | 组件内 ref / reactive | 仅本组件 | 表单值、loading、弹窗开关 | 零 |
| 2 | props / emit | 父子组件 | 父传子展示数据,子通知父事件 | 低 |
| 3 | provide / inject | 一棵子组件树 | 深层透传:主题、当前布局配置 | 中 |
| 4 | Pinia(全局 store) | 整个应用 | 登录用户、购物车、跨页面共享数据 | 中高 |
优劣分析
- 组件内 ref:默认选择,90% 的状态属于这里。优点是隔离好、好理解;缺点是出了组件就没人能访问。
- props/emit:父子通信的正路。超过 2 层的传递(祖孙)就会产生「中间人被迫转发」的噪音,那是信号:该换方案了。
- provide/inject:解决深层透传,但依赖关系变隐式(不看代码不知道数据从哪来),只适合「一整棵子树共享」的场景。
- Pinia:全局可见、DevTools 可追踪、支持持久化插件。代价是引入全局耦合——乱用会让数据流变成蛛网。
怎么选(决策顺序,从上往下第一条命中即停)
- 只有本组件用 → 组件内
ref - 父子之间 →
props/emit - 祖孙/深层子树,且是「环境类」数据 →
provide/inject - 两个以上不相干的页面/组件要读写同一份数据 → Pinia
新手最常见错误
一上来就建一堆全局 store,把什么都放进去。全局状态是最后手段:每加一个全局状态,都要能回答「为什么它必须全局」。
Pinia 最小示例
ts
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, double, increment }
})vue
<!-- 任何组件里 -->
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<button @click="counter.increment">{{ counter.count }} / {{ counter.double }}</button>
</template>判断标准一句话
「关掉这个页面,这份数据还需要存在吗?」需要 → 全局(Pinia 或 URL);不需要 → 就地解决。