Vue Props 与 Emit

组件化的核心是隔离——每个组件管自己的数据和样式。但隔离不等于孤立:父子组件之间必须有通信机制。Vue 提供两个核心 API:Props 让父组件向子组件传数据,Emit 让子组件向父组件发事件。本章详细讲清。

1. Props:父传子的数据通道

Props 是组件的输入。父组件通过属性的形式把数据传给子组件,子组件用 defineProps 接收。

<!-- UserCard.vue 子组件 -->
<template>
  <div class="card">
    <h3>{{ name }}</h3>
    <p>年龄:{{ age }}</p>
  </div>
</template>

<script setup>
// 写法一:字符串数组(简单但无校验)
defineProps(["name", "age"]);

// 写法二:对象 + 类型(推荐,有警告)
defineProps({
  name: String,
  age: Number,
});
</script>

<!-- 父组件 -->
<template>
  <!-- 把数据通过 props 传给子组件 -->
  <UserCard :name="user.name" :age="user.age" />
  <UserCard name="小红" :age="18" />
</template>

<script setup>
import { ref } from "vue";
import UserCard from "./UserCard.vue";
const user = ref({ name: "小明", age: 20 });
</script>

几个要点:

2. Props 的详细校验

对象写法可以给每个 prop 加更多约束——类型、必填、默认值、自定义校验。Vue 在开发模式下会检查这些约束,违反时打印警告。

<script setup>
// 详细的 props 定义:类型、必填、默认值、自定义校验
defineProps({
  // 基础类型检查
  name: String,
  age: Number,
  isAdmin: Boolean,

  // 多个允许的类型
  id: [String, Number],

  // 必填 + 类型
  email: {
    type: String,
    required: true,
  },

  // 有默认值
  role: {
    type: String,
    default: "user",
  },

  // 对象/数组的默认值要用工厂函数
  tags: {
    type: Array,
    default: () => [],
  },

  // 自定义校验函数
  rating: {
    validator: (val) => val >= 1 && val <= 5,
  },
});
</script>

<!-- 仅运行时声明(无 TS 类型)。
     需要 TS 类型推导时用 withDefaults + 泛型 -->

3. TypeScript 写法

用 TypeScript 时,可以用泛型直接写 props 的类型,IDE 自动补全和类型检查都更准:

<script setup lang="ts">
// TypeScript 写法:用泛型直接写类型
interface Props {
  name: string;
  age?: number;          // 可选
  tags?: string[];
}

const props = withDefaults(defineProps<Props>(), {
  age: 18,
  tags: () => ["default"],
});

// 现在 props.name 是 string 类型,IDE 自动补全
console.log(props.name.toUpperCase());
</script>

需要默认值时用 withDefaults(因为泛型写法不能直接和对象写法合并)。这是 Vue 3 + TS 项目的标准写法。

4. 单向数据流(重要!)

Vue 的 props 是单向的:父组件改了,子组件自动更新;但子组件不能直接改 props。这是 Vue 的核心约束:

这个约束的好处是数据流向清晰——永远父 → 子,不会出现"两边互相改"的混乱局面。大型项目里这是减少 bug 的关键。

5. Emit:子传父的事件通道

子组件想通知父组件"发生了什么"(被点击了、值变了),用 emit 触发自定义事件。父组件用 @事件名 监听,和原生 DOM 事件一样的语法。

<!-- 子组件 Counter.vue -->
<template>
  <button @click="onClick">+1</button>
</template>

<script setup>
import { ref } from "vue";

// 声明可以触发的事件(可选,但推荐——有校验和 IDE 提示)
const emit = defineEmits(["increase", "reset"]);

const count = ref(0);

function onClick() {
  count.value++;
  // 触发 increase 事件,把当前 count 作为数据传出去
  emit("increase", count.value);
}
</script>

<!-- 父组件 -->
<template>
  <Counter @increase="onIncrease" @reset="onReset" />
  <p>当前总数:{{ total }}</p>
</template>

<script setup>
import { ref } from "vue";
import Counter from "./Counter.vue";
const total = ref(0);
function onIncrease(n) {
  total.value = n;
}
function onReset() {
  total.value = 0;
}
</script>

几个要点:

6. Emit 的 TypeScript 写法

用 TS 时可以声明每个事件的参数类型,触发时类型不匹配会报错:

<script setup lang="ts">
// TypeScript 写法:声明事件的 payload 类型
const emit = defineEmits<{
  // 签名:事件名 + 参数类型
  (e: "increase", count: number): void;
  (e: "change", name: string, age: number): void;
  (e: "reset"): void;
}>();

emit("increase", 1);          // ✅
emit("increase", "wrong");    // ❌ 类型错误
</script>

7. v-model 在组件上的应用

第 6 章我们提过自定义组件可以用 v-model。现在能讲清原理了——它就是 props + emit 的语法糖:

<!-- 子组件 CustomInput.vue -->
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script setup>
defineProps(["modelValue"]);
defineEmits(["update:modelValue"]);
</script>

<!-- 父组件 -->
<template>
  <CustomInput v-model="msg" />
  <!-- 等价于: -->
  <CustomInput
    :model-value="msg"
    @update:model-value="msg = $event"
  />
</template>

<!-- 多 v-model:Vue 3 支持给每个 v-model 加参数 -->
<template>
  <UserForm v-model:name="name" v-model:age="age" />
</template>

子组件接收 modelValue prop,触发 update:modelValue 事件。父组件的 v-model 自动展开成这两个绑定。Vue 3 还支持多个 v-model(v-model:namev-model:age),让一个组件暴露多个双向绑定通道。

8. 父子通信的完整心智模型

9. 实战:可关闭的对话框

把 props 和 emit 组合起来,写一个最常见的对话框组件:

<!-- Dialog.vue -->
<template>
  <div v-if="visible" class="dialog">
    <h3>{{ title }}</h3>
    <slot></slot>
    <button @click="close">关闭</button>
  </div>
</template>

<script setup>
defineProps({
  visible: Boolean,
  title: { type: String, required: true },
});
const emit = defineEmits(["close"]);

function close() {
  emit("close");   // 通知父组件:用户要关对话框了
}
</script>

<!-- 父组件用法 -->
<template>
  <button @click="show = true">打开</button>
  <Dialog :visible="show" title="提示" @close="show = false">
    <p>你确定要删除吗?</p>
  </Dialog>
</template>

<script setup>
import { ref } from "vue";
import Dialog from "./Dialog.vue";
const show = ref(false);
</script>

注意这种设计:子组件不直接改 props(visible),而是通过 emit 通知父组件"用户要关了",由父组件真正改 show。这就是单向数据流的优雅之处——数据归属清晰,组件可复用、可测试。

小结

这一章你掌握了 Vue 父子组件通信的核心:Props 传数据(单向)、Emit 发事件、v-model 是 props + emit 的语法糖。理解单向数据流是写出可维护 Vue 应用的关键。下一篇我们看组件从生到死的生命周期

← 上一篇 Vue 组件基础

下一篇 Vue 生命周期

✈️💬