Skip to content

状态管理方案对比 ​

是什么 ​

「状态」= 会随时间变化、且界面要跟着变的数据(登录用户、购物车、表单草稿、弹窗开关……)。

状态管理的全部问题其实是:这个状态放在哪、谁能看到它、改它。放错位置的典型症状:props 一层层往下传五六层、两个不相干组件要同步一份数据、刷新后状态乱套。

四个层级(从小到大) ​

层级方案作用范围适合放什么成本
1组件内 ref / reactive仅本组件表单值、loading、弹窗开关零
2props / emit父子组件父传子展示数据,子通知父事件低
3provide / inject一棵子组件树深层透传:主题、当前布局配置中
4Pinia(全局 store)整个应用登录用户、购物车、跨页面共享数据中高

优劣分析 ​

  • 组件内 ref:默认选择,90% 的状态属于这里。优点是隔离好、好理解;缺点是出了组件就没人能访问。
  • props/emit:父子通信的正路。超过 2 层的传递(祖孙)就会产生「中间人被迫转发」的噪音,那是信号:该换方案了。
  • provide/inject:解决深层透传,但依赖关系变隐式(不看代码不知道数据从哪来),只适合「一整棵子树共享」的场景。
  • Pinia:全局可见、DevTools 可追踪、支持持久化插件。代价是引入全局耦合——乱用会让数据流变成蛛网。

怎么选(决策顺序,从上往下第一条命中即停) ​

  1. 只有本组件用 → 组件内 ref
  2. 父子之间 → props / emit
  3. 祖孙/深层子树,且是「环境类」数据 → provide / inject
  4. 两个以上不相干的页面/组件要读写同一份数据 → 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);不需要 → 就地解决。