在前端开发中,我们几乎绕不开一个核心问题:状态(state)该放在哪里?
随着项目复杂度的提升,状态的存放位置也会经历一次次"升级":
子组件 → 父组件 → Hook(组合式函数)→ Pinia(全局状态管理)
这篇文章,我会带你一步步拆解这个"状态提升"的演进过程,并结合VUE代码示例,帮你理解每一次升级背后的动机和设计思想。
一、第一阶段:状态在子组件中(局部状态)
在项目早期,我们通常会把状态直接写在子组件内部。
示例:一个计数器组件
xml
<!-- Counter.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
特点
- 状态封装在组件内部
- 简单直观
- 适合完全独立的 UI 组件
问题
如果有两个组件都需要用到这个 count 呢?
比如:
xml
<Counter />
<Display />
Display 组件也想显示这个 count,怎么办?
这时我们就需要第一次升级。
二、第二阶段:从子组件提升到父组件
当多个子组件共享状态时,我们会把状态"提升"到它们的共同父组件。
这和 React 的"状态提升"思想是一致的。
父组件管理状态
xml
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'
import Display from './Display.vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
<template>
<Counter :count="count" @increment="increment" />
<Display :count="count" />
</template>
子组件只负责展示和触发
xml
<!-- Counter.vue -->
<script setup>
defineProps({
count: Number
})
defineEmits(['increment'])
</script>
<template>
<button @click="$emit('increment')">+1</button>
</template>
优点
- 状态集中管理
- 数据流清晰(单向数据流)
缺点
-
层级一深就会出现:
- props drilling(层层传参)
- 事件层层冒泡
-
父组件变得"臃肿"
当项目规模扩大后,这种方式开始吃力。
于是我们进行第二次升级。
三、第三阶段:从父组件提升到 Hook(组合式函数)
在 Vue 3 中,Composition API 让我们可以把逻辑抽离成 Hook(组合式函数)。
我们把状态抽离到一个独立文件中。
创建一个 useCounter.ts
javascript
// useCounter.ts
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment
}
}
在组件中使用
xml
<script setup>
import { useCounter } from './useCounter'
const { count, increment } = useCounter()
</script>
优点
- 逻辑复用
- 代码结构更清晰
- 组件变"干净"
- 可测试性更强
但问题来了
如果两个组件都调用 useCounter():
ini
const a = useCounter()
const b = useCounter()
它们的 count 是:
❌ 不共享的
每调用一次都会创建新的状态实例。
如果我们希望多个组件共享同一个状态怎么办?
这时候,Hook 已经不够用了。
于是我们迎来终极升级。
四、第四阶段:从 Hook 升级到 Pinia
当状态需要在多个页面、多个模块、多个层级中共享时,我们就需要真正的状态管理工具。
在 Vue 生态中,主流选择是:
- Vuex(旧)
- Pinia(官方推荐)
这里我们使用 Pinia。
什么是 Pinia?
Pinia 是 Vue 官方推荐的状态管理库,支持 Vue 3,API 设计非常现代化。
它的理念是:
Store = 可复用的全局 Hook
创建一个 Counter Store
javascript
// stores/counter.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => {
count.value++
}
return { count, increment }
})
在组件中使用
xml
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<button @click="counter.increment">
{{ counter.count }}
</button>
</template>
关键特性
- 所有组件共享同一个 store
- 自动响应式
- DevTools 支持
- 模块化管理
状态升级的本质
我们来总结一下这四个阶段:
| 阶段 | 状态位置 | 适用场景 | 缺点 |
|---|---|---|---|
| 子组件 | 组件内部 | 完全独立组件 | 无法共享 |
| 父组件 | 父级 | 局部共享 | 层级深会混乱 |
| Hook | 逻辑抽离 | 逻辑复用 | 默认不共享 |
| Pinia | 全局 Store | 跨页面共享 | 增加架构复杂度 |
设计哲学:状态放在哪里?
可以用一句话概括:
状态应该放在"刚好需要它的最上层"
- 只一个组件用 → 放子组件
- 两个兄弟组件用 → 放父组件
- 多个地方用但不共享 → Hook
- 全局共享 → Pinia
这是一种"按需升级"的架构策略。
不要一开始就上 Pinia
不要直接提升到pinia,这会带来:
- 不必要的全局耦合
- 难以维护
- 状态污染
记住:
全局状态是一种"权力",不要滥用。
应该从下至上,找到最合适的地方,随着需求的变更,代码也跟随变更。
架构升级的思维模型
这个升级过程,本质上体现的是:
- 局部化 → 抽象化 → 全局化
- 组件驱动 → 逻辑驱动 → 状态驱动
这也是现代前端架构演进的核心路线。
结语
Vue 的状态管理不是非黑即白的选择,而是一个"渐进增强"的过程。
当你理解了:
- 为什么提升状态
- 什么时候该提升
- 提升的边界在哪里
你就真正掌握了 Vue 状态管理的设计思想。
思考
- 如果所有的父子组件都需要这个状态呢?(Provide/Inject)