Vue 3 状态共享:简单场景不用 Pinia 也能搞定
在 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
npm install mitt2. 创建事件总线
// src/eventBus.js
import mitt from 'mitt'
const emitter = mitt()
export default emitter3. 发送事件
<!-- 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:
数据复杂:有大量全局状态,涉及多个业务模块
需要高级功能:比如时间旅行调试、状态持久化
团队协作:需要统一的状态管理规范
复杂逻辑:有大量异步操作、中间件处理
需要插件:比如要集成请求库、表单验证等
性能考虑
少量数据:上面方法性能都很好
大量数据频繁更新: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 给了我们很多灵活的选择。关键是根据实际需求选择合适的方法:
简单场景:直接用 ref/reactive 导出,最省事
需要封装:用自定义组合函数,结构更好
组件树内共享:用 provide/inject,作用域明确
简单通信:用事件总线,解耦组件
记住一个原则:能用简单方法解决的,就不要用复杂方案。等真的需要 Pinia 那些高级功能时,再引入也不迟。
好的代码不是用最复杂的工具,而是用最合适的工具。根据你的项目大小和需求,选择最合适的状态管理方式,才能让项目既容易维护,又不会过度设计。
本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!