Appearance
23|Vue 组件通信与状态管理
页面拆成多个组件后,组件之间需要交换数据:父组件向子组件传配置、子组件向父组件通知事件、没有父子关系的组件共享状态。Vue 提供 props/emit、provide/inject 和 Pinia 三种层级的通信方案,分别应对不同复杂度的场景。
一、Props 父传子
父组件通过属性向子组件传递数据:
vue
<!-- Parent.vue -->
<template>
<ServerCard :server="serverData" />
</template>
<script setup>
import { ref } from "vue";
import ServerCard from "./ServerCard.vue";
const serverData = ref({
hostname: "web-01",
status: "running",
});
</script>vue
<!-- ServerCard.vue -->
<template>
<div class="card">
<h3>{{ server.hostname }}</h3>
<span :class="server.status">{{ server.status }}</span>
</div>
</template>
<script setup>
const props = defineProps({
server: {
type: Object,
required: true,
},
});
// 在 script 中访问需要用 props.server
console.log(props.server.hostname);
</script>defineProps 是编译器宏,不需要从 vue 导入。可以声明类型、是否必填、默认值:
js
defineProps({
title: {
type: String,
required: true,
},
count: {
type: Number,
default: 0,
},
tags: {
type: Array,
default: () => [], // 对象/数组默认值必须用工厂函数
},
});Props 是只读的,子组件不能修改父组件传过来的数据:
js
// 错误
props.server.status = "stopped";
// 正确:把数据复制到本地状态,或者通知父组件修改
const localStatus = ref(props.server.status);二、Emit 子传父
子组件通过事件通知父组件:
vue
<!-- ServerCard.vue -->
<template>
<button @click="handleRestart">重启</button>
</template>
<script setup>
const emit = defineEmits(["restart"]);
function handleRestart() {
emit("restart", props.server.id); // 第二个参数是传给父组件的数据
}
</script>vue
<!-- Parent.vue -->
<template>
<ServerCard
:server="server"
@restart="onRestart"
/>
</template>
<script setup>
function onRestart(serverId) {
console.log(`重启服务器 ${serverId}`);
}
</script>defineEmits 声明组件会触发哪些事件,便于 IDE 提示和类型检查。
三、Provide / Inject
跨多层组件传递数据时,props 需要层层传递(prop drilling)。provide/inject 让祖先组件直接向后代组件提供数据:
vue
<!-- App.vue(祖先) -->
<script setup>
import { provide, ref } from "vue";
const user = ref({ name: "admin", role: "admin" });
provide("user", user);
</script>vue
<!-- DeepChild.vue(深层后代) -->
<script setup>
import { inject } from "vue";
const user = inject("user");
console.log(user.value.name); // "admin"
</script>provide/inject 适合全局配置、主题、用户状态等不常变的数据。数据仍应保持响应式——如果 provide 的是 ref 或 reactive,后代组件修改它会影响所有使用者。
四、Pinia 状态管理
当多个组件需要读写同一份状态时,provide/inject 或事件总线会变得混乱。Pinia 是 Vue 官方推荐的状态管理库,用 Store 集中管理共享状态。
安装和创建 Store
bash
npm install piniajs
// stores/user.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useUserStore = defineStore("user", () => {
// State
const name = ref("");
const role = ref("guest");
// Getter
const isAdmin = computed(() => role.value === "admin");
// Action
function login(userData) {
name.value = userData.name;
role.value = userData.role;
}
function logout() {
name.value = "";
role.value = "guest";
}
return { name, role, isAdmin, login, logout };
});在组件中使用
vue
<template>
<div v-if="user.isAdmin">管理面板</div>
<p>当前用户: {{ user.name }}</p>
<button @click="user.logout">退出</button>
</template>
<script setup>
import { useUserStore } from "../stores/user.js";
const user = useUserStore();
</script>Store 中的 ref 和 computed 在组件中直接解构会失去响应性,要用 storeToRefs:
js
import { storeToRefs } from "pinia";
const userStore = useUserStore();
const { name, isAdmin } = storeToRefs(userStore); // 保持响应性
const { login, logout } = userStore; // 方法可以直接解构Pinia 适用场景
| 场景 | 方案 |
|---|---|
| 父子组件简单传值 | Props + Emit |
| 跨多层但结构稳定的组件 | Provide + Inject |
| 多个页面共享的用户状态、全局配置 | Pinia Store |
五、常见错误
直接修改 props
vue
<script setup>
const props = defineProps(["count"]);
// 错误:props 是只读的
props.count++;
// 正确:用 emit 通知父组件,或复制到本地状态
const localCount = ref(props.count);
localCount.value++;
</script>解构 props 失去响应性
js
// 错误:title 变成普通字符串,不会更新
const { title } = defineProps(["title"]);
// 正确:用 toRefs,或在模板中直接用
import { toRefs } from "vue";
const { title } = toRefs(defineProps(["title"]));
console.log(title.value);在 Pinia Store 中忘记 return
js
export const useStore = defineStore("test", () => {
const count = ref(0);
// 错误:忘记 return,组件中访问不到
// return { count };
});Provide 非响应式数据
js
// 错误:提供普通对象,后代修改不会通知其他使用者
provide("config", { theme: "dark" });
// 正确:用 reactive 或 ref
provide("config", reactive({ theme: "dark" }));