文章

状态提升:前端开发中的状态管理的设计思想

状态提升:前端开发中的状态管理的设计思想

在前端开发中,我们几乎绕不开一个核心问题:状态(state)该放在哪里?

随着项目复杂度的提升,状态的存放位置也会经历一次次“升级”:

子组件 → 父组件 → Hook(组合式函数)→ Pinia(全局状态管理) 这篇文章,我会带你一步步拆解这个“状态提升”的演进过程,并结合VUE代码示例,帮你理解每一次升级背后的动机和设计思想。

一、第一阶段:状态在子组件中(局部状态)

在项目早期,我们通常会把状态直接写在子组件内部。

示例:一个计数器组件

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { ref } from 'vue'

const count = ref(0)

const increment = () => {
  count.value++
}

    <p>9</p>
    <button @click="increment">+1</button>

特点

  • 状态封装在组件内部- 简单直观- 适合完全独立的 UI 组件

    问题

如果有两个组件都需要用到这个 count 呢?

比如:

1
2
<Counter />
<Display />

Display 组件也想显示这个 count,怎么办?

这时我们就需要第一次升级。

二、第二阶段:从子组件提升到父组件

当多个子组件共享状态时,我们会把状态“提升”到它们的共同父组件。

这和 React 的“状态提升”思想是一致的。

父组件管理状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'
import Display from './Display.vue'

const count = ref(0)

const increment = () => {
  count.value++
}

  <Counter :count="count" @increment="increment" />
  <Display :count="count" />

子组件只负责展示和触发

1
2
3
4
5
6
7
8
9
10
<script setup>
defineProps({
  count: Number
})

defineEmits(['increment'])

  <button @click="$emit('increment')">+1</button>

优点

  • 状态集中管理- 数据流清晰(单向数据流)

    缺点

  • 层级一深就会出现:

props drilling(层层传参)- 事件层层冒泡 父组件变得“臃肿”

当项目规模扩大后,这种方式开始吃力。

于是我们进行第二次升级。

三、第三阶段:从父组件提升到 Hook(组合式函数)

在 Vue 3 中,Composition API 让我们可以把逻辑抽离成 Hook(组合式函数)。

我们把状态抽离到一个独立文件中。

创建一个 useCounter.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// useCounter.ts
import { ref } from 'vue'

export function useCounter() {
  const count = ref(0)

  const increment = () => {
    count.value++
  }

  return {
    count,
    increment
  }
}

在组件中使用

1
2
3
4
5
<script setup>
import { useCounter } from './useCounter'

const { count, increment } = useCounter()

优点

  • 逻辑复用- 代码结构更清晰- 组件变“干净”- 可测试性更强

    但问题来了

如果两个组件都调用 useCounter()

1
2
const a = useCounter()
const b = useCounter()

它们的 count 是:

❌ 不共享的 每调用一次都会创建新的状态实例。

如果我们希望多个组件共享同一个状态怎么办?

这时候,Hook 已经不够用了。

于是我们迎来终极升级。

四、第四阶段:从 Hook 升级到 Pinia

当状态需要在多个页面、多个模块、多个层级中共享时,我们就需要真正的状态管理工具。

在 Vue 生态中,主流选择是:

  • Vuex(旧)- Pinia(官方推荐) 这里我们使用 Pinia。

什么是 Pinia?

Pinia 是 Vue 官方推荐的状态管理库,支持 Vue 3,API 设计非常现代化。

它的理念是:

Store = 可复用的全局 Hook

创建一个 Counter Store

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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 }
})

在组件中使用

1
2
3
4
5
6
7
8
<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

  <button @click="counter.increment">
    

关键特性

  • 所有组件共享同一个 store- 自动响应式- DevTools 支持- 模块化管理

    状态升级的本质

我们来总结一下这四个阶段:

| 阶段 | 状态位置 | 适用场景 | 缺点 || — | — | — | — || 子组件 | 组件内部 | 完全独立组件 | 无法共享 || 父组件 | 父级 | 局部共享 | 层级深会混乱 || Hook | 逻辑抽离 | 逻辑复用 | 默认不共享 || Pinia | 全局 Store | 跨页面共享 | 增加架构复杂度 |

设计哲学:状态放在哪里?

可以用一句话概括:

状态应该放在“刚好需要它的最上层”

  • 只一个组件用 → 放子组件- 两个兄弟组件用 → 放父组件- 多个地方用但不共享 → Hook- 全局共享 → Pinia 这是一种“按需升级”的架构策略。

不要一开始就上 Pinia

不要直接提升到pinia,这会带来:

  • 不必要的全局耦合- 难以维护- 状态污染 记住:

全局状态是一种“权力”,不要滥用。 应该从下至上,找到最合适的地方,随着需求的变更,代码也跟随变更。

架构升级的思维模型

这个升级过程,本质上体现的是:

  • 局部化 → 抽象化 → 全局化- 组件驱动 → 逻辑驱动 → 状态驱动 这也是现代前端架构演进的核心路线。

结语

Vue 的状态管理不是非黑即白的选择,而是一个“渐进增强”的过程。

当你理解了:

  • 为什么提升状态- 什么时候该提升- 提升的边界在哪里 你就真正掌握了 Vue 状态管理的设计思想。

思考

  • 如果所有的父子组件都需要这个状态呢?(Provide/Inject)
本文由作者按照 CC BY 4.0 进行授权