Skip to content

23|Vue 组件通信与状态管理

页面拆成多个组件后,组件之间需要交换数据:父组件向子组件传配置、子组件向父组件通知事件、没有父子关系的组件共享状态。Vue 提供 props/emitprovide/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 的是 refreactive,后代组件修改它会影响所有使用者。

四、Pinia 状态管理

当多个组件需要读写同一份状态时,provide/inject 或事件总线会变得混乱。Pinia 是 Vue 官方推荐的状态管理库,用 Store 集中管理共享状态。

安装和创建 Store

bash
npm install pinia
js
// 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 中的 refcomputed 在组件中直接解构会失去响应性,要用 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" }));