Vue 3 状态共享:简单场景不用 Pinia 也能搞定

摘要:在 Vue 3 项目里,经常遇到需要在不同组件之间共享数据的情况。如果只是少量数据,其实完全没必要引入 Pinia 或 Vuex 这样完整的状态管理库。用更简单的方法,既能减少项目依赖,又能让代码更清晰。

在 Vue 3 项目里,经常遇到需要在不同组件之间共享数据的情况。如果只是少量数据,其实完全没必要引入 Pinia 或 Vuex 这样完整的状态管理库。用更简单的方法,既能减少项目依赖,又能让代码更清晰。

下面介绍几种实用的方法,帮你解决常见的数据共享问题。


方法一:直接用 ref/reactive 导出(最常用)

这是最简单直接的方法。创建一个文件,在里面定义响应式数据,然后导出给其他组件使用。

1. 创建全局状态文件

// src/globalState.js
import { ref, reactive } from 'vue'

// 定义一个计数器
export const globalCount = ref(0)

// 定义一个用户对象
export const globalUser = reactive({
  name: '张三',
  age: 18,
  email: 'zhangsan@example.com'
})

// 定义一个主题设置
export const theme = ref('light')

2. 在组件中使用

<!-- UserProfile.vue -->
<script setup>
import { globalCount, globalUser } from '@/globalState'

function addAge() {
  globalUser.age++
}

function resetCount() {
  globalCount.value = 0
}
</script>

<template>
  <div>
    <h3>用户信息</h3>
    <p>姓名:{{ globalUser.name }}</p>
    <p>年龄:{{ globalUser.age }}</p>
    <button @click="addAge">增加年龄</button>
    
    <p>全局计数:{{ globalCount }}</p>
    <button @click="resetCount">重置计数</button>
  </div>
</template>

3. 在另一个组件中也使用

<!-- Counter.vue -->
<script setup>
import { globalCount } from '@/globalState'

function increment() {
  globalCount.value++
}
</script>

<template>
  <div>
    <p>当前计数:{{ globalCount }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

这样做的好处:

  • 简单直接,不需要学习新概念

  • 数据是响应式的,一处修改,所有用到的地方自动更新

  • 适合跨组件、跨路由的少量数据共享

需要注意:

  • 数据是全局的,要小心命名冲突

  • 适合简单的场景,复杂逻辑可能不好维护


方法二:用自定义组合函数封装

如果你想对状态访问做一些控制,或者未来可能要扩展功能,可以用自定义组合函数来封装。

1. 创建组合函数

// src/composables/useGlobalData.js
import { ref, computed } from 'vue'

// 定义私有状态
const count = ref(0)
const todos = ref([])

// 导出组合函数
export function useGlobalData() {
  // 计算属性示例
  const todoCount = computed(() => todos.value.length)
  
  // 操作方法
  function addTodo(todo) {
    todos.value.push(todo)
  }
  
  function clearTodos() {
    todos.value = []
  }
  
  return {
    // 状态
    count,
    todos,
    
    // 计算属性
    todoCount,
    
    // 方法
    addTodo,
    clearTodos,
    increment: () => count.value++,
    decrement: () => count.value--,
    reset: () => count.value = 0
  }
}

2. 在组件中使用

<!-- TodoList.vue -->
<script setup>
import { useGlobalData } from '@/composables/useGlobalData'

const { 
  todos, 
  todoCount, 
  addTodo, 
  clearTodos 
} = useGlobalData()

const newTodo = ref('')

function handleAdd() {
  if (newTodo.value.trim()) {
    addTodo(newTodo.value)
    newTodo.value = ''
  }
}
</script>

<template>
  <div>
    <h3>待办事项 ({{ todoCount }})</h3>
    <input v-model="newTodo" @keyup.enter="handleAdd">
    <button @click="handleAdd">添加</button>
    <button @click="clearTodos">清空</button>
    
    <ul>
      <li v-for="(todo, index) in todos" :key="index">
        {{ todo }}
      </li>
    </ul>
  </div>
</template>

这种方式的优势:

  • 保持了组合式 API 的风格

  • 可以封装逻辑和计算方法

  • 未来要迁移到 Pinia 时,改动很小

  • 代码更有组织性


方法三:用 provide/inject(适合组件树内部共享)

如果数据只需要在某个组件及其子组件之间共享,用 provide/inject 更合适。这样不会污染全局。

1. 在父组件提供数据

<!-- App.vue -->
<script setup>
import { ref, provide } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 定义要共享的数据
const theme = ref('light')
const userPreferences = ref({
  language: 'zh-CN',
  notifications: true
})

// 提供数据
provide('theme', theme)
provide('userPreferences', userPreferences)

// 提供修改方法
function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

function updateLanguage(lang) {
  userPreferences.value.language = lang
}

provide('toggleTheme', toggleTheme)
provide('updateLanguage', updateLanguage)
</script>

<template>
  <div :class="theme">
    <ChildComponent />
  </div>
</template>

<style>
.light { background: white; color: black; }
.dark { background: #333; color: white; }
</style>

2. 在子组件中使用

<!-- ChildComponent.vue -->
<script setup>
import { inject } from 'vue'
import GrandChildComponent from './GrandChildComponent.vue'

// 注入数据
const theme = inject('theme')
const userPreferences = inject('userPreferences')
const toggleTheme = inject('toggleTheme')
const updateLanguage = inject('updateLanguage')
</script>

<template>
  <div>
    <p>当前主题:{{ theme }}</p>
    <p>语言设置:{{ userPreferences.language }}</p>
    <button @click="toggleTheme">切换主题</button>
    <button @click="() => updateLanguage('en-US')">
      切换为英文
    </button>
    
    <!-- 孙子组件也能拿到这些数据 -->
    <GrandChildComponent />
  </div>
</template>

3. 孙子组件也能用

<!-- GrandChildComponent.vue -->
<script setup>
import { inject } from 'vue'

const theme = inject('theme')
const userPreferences = inject('userPreferences')
</script>

<template>
  <div>
    <p>深层的组件也能访问:{{ theme }} 主题</p>
    <p>通知设置:{{ userPreferences.notifications ? '开启' : '关闭' }}</p>
  </div>
</template>

适用场景:

  • 主题切换

  • 用户偏好设置

  • 多语言支持

  • 需要在组件树中传递,但不想用 props 一层层传

重要提醒:

  • 默认情况下,inject 拿到的不是响应式的

  • 要传响应式数据,必须传 ref 或 reactive 对象

  • 建议用 Symbol 作为 key,避免命名冲突


方法四:用事件总线(Event Bus)

Vue 3 移除了 $on、$off 等方法,但我们可以用 mitt 这样的小库来实现事件总线。

1. 安装 mitt

bash
npm install mitt

2. 创建事件总线

// src/eventBus.js
import mitt from 'mitt'

const emitter = mitt()

export default emitter

3. 发送事件

<!-- ComponentA.vue -->
<script setup>
import emitter from '@/eventBus'

function sendMessage() {
  emitter.emit('message', {
    text: '你好!',
    time: new Date()
  })
}
</script>

<template>
  <button @click="sendMessage">发送消息</button>
</template>

4. 接收事件

<!-- ComponentB.vue -->
<script setup>
import { onMounted, onUnmounted, ref } from 'vue'
import emitter from '@/eventBus'

const messages = ref([])

function handleMessage(data) {
  messages.value.push(data)
}

onMounted(() => {
  emitter.on('message', handleMessage)
})

onUnmounted(() => {
  emitter.off('message', handleMessage)
})
</script>

<template>
  <div>
    <h3>收到的消息:</h3>
    <div v-for="(msg, index) in messages" :key="index">
      {{ msg.text }} - {{ msg.time }}
    </div>
  </div>
</template>

适合场景:

  • 组件之间需要通信,但没有直接关系

  • 一对多的通知

  • 简单的状态同步


实际项目中的选择建议

什么情况下用哪种方法?

1. 少量全局配置(如主题、用户信息)

  • 推荐:直接导出 ref/reactive

  • 理由:简单直接,不需要复杂逻辑

2. 需要封装的业务逻辑

  • 推荐:自定义组合函数

  • 理由:便于维护和测试,结构清晰

3. 组件树内部的共享

  • 推荐:provide/inject

  • 理由:作用域明确,不污染全局

4. 组件间简单通信

  • 推荐:事件总线

  • 理由:解耦,适合没有直接关系的组件

什么时候该用 Pinia?

虽然上面方法能满足大多数需求,但有些情况确实需要 Pinia:

  1. 数据复杂:有大量全局状态,涉及多个业务模块

  2. 需要高级功能:比如时间旅行调试、状态持久化

  3. 团队协作:需要统一的状态管理规范

  4. 复杂逻辑:有大量异步操作、中间件处理

  5. 需要插件:比如要集成请求库、表单验证等

性能考虑

  • 少量数据:上面方法性能都很好

  • 大量数据频繁更新:Pinia 可能有更好的优化

  • 组件层级深:provide/inject 性能比 props 逐层传递好


完整示例:用户登录状态管理

下面用一个实际例子展示如何管理用户登录状态:

// src/composables/useAuth.js
import { ref, computed } from 'vue'

// 状态
const user = ref(null)
const token = ref(localStorage.getItem('token') || '')

// 组合函数
export function useAuth() {
  // 计算属性
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name || '游客')
  
  // 方法
  async function login(username, password) {
    // 模拟登录请求
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ username, password })
    })
    
    const data = await response.json()
    
    if (data.success) {
      user.value = data.user
      token.value = data.token
      localStorage.setItem('token', data.token)
      return true
    }
    
    return false
  }
  
  function logout() {
    user.value = null
    token.value = ''
    localStorage.removeItem('token')
  }
  
  return {
    // 状态
    user,
    token,
    
    // 计算属性
    isLoggedIn,
    userName,
    
    // 方法
    login,
    logout
  }
}

总结

Vue 3 给了我们很多灵活的选择。关键是根据实际需求选择合适的方法:

  1. 简单场景:直接用 ref/reactive 导出,最省事

  2. 需要封装:用自定义组合函数,结构更好

  3. 组件树内共享:用 provide/inject,作用域明确

  4. 简单通信:用事件总线,解耦组件

记住一个原则:能用简单方法解决的,就不要用复杂方案。等真的需要 Pinia 那些高级功能时,再引入也不迟。

好的代码不是用最复杂的工具,而是用最合适的工具。根据你的项目大小和需求,选择最合适的状态管理方式,才能让项目既容易维护,又不会过度设计。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_13278