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

14 阅读4分钟

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

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

子组件 → 父组件 → Hook(组合式函数)→ Pinia(全局状态管理)

这篇文章,我会带你一步步拆解这个“状态提升”的演进过程,并结合VUE代码示例,帮你理解每一次升级背后的动机和设计思想。


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

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

示例:一个计数器组件

<!-- 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 呢?

比如:

<Counter />
<Display />

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

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


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

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

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

父组件管理状态

<!-- 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>

子组件只负责展示和触发

<!-- 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

// useCounter.ts
import { ref } from 'vue'

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

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

  return {
    count,
    increment
  }
}

在组件中使用

<script setup>
import { useCounter } from './useCounter'

const { count, increment } = useCounter()
</script>

优点

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

但问题来了

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

const a = useCounter()
const b = useCounter()

它们的 count 是:

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

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

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

于是我们迎来终极升级。


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

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

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

  • Vuex(旧)
  • Pinia(官方推荐)

这里我们使用 Pinia。


什么是 Pinia?

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

它的理念是:

Store = 可复用的全局 Hook


创建一个 Counter Store

// 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 }
})

在组件中使用

<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)