跳到主要内容

3.5 组件、props 与 emits

本章目标: 将任务行提取为子组件,让父组件拥有数据、子组件表达操作意图。

先定义模型​

src/types/task.ts
export interface TaskItem {
id: number;
title: string;
isDone: boolean;
}

子组件接收数据,发出事件​

src/components/TaskRow.vue
<script setup lang="ts">
import type { TaskItem } from '../types/task';

const props = defineProps<{ task: TaskItem }>();
const emit = defineEmits<{
toggle: [id: number];
remove: [id: number];
}>();
</script>

<template>
<li>
<label>
<input type="checkbox" :checked="props.task.isDone"
@change="emit('toggle', props.task.id)" />
{{ props.task.title }}
</label>
<button type="button" @click="emit('remove', props.task.id)">删除</button>
</li>
</template>

defineProps 和 defineEmits 是 <script setup> 的编译器宏,不需要从 Vue 导入。类型参数帮助检查属性和事件 payload。

父组件拥有状态​

src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import TaskRow from './components/TaskRow.vue';
import type { TaskItem } from './types/task';

const tasks = ref<TaskItem[]>([
{ id: 1, title: '学习组件通信', isDone: false },
]);

function toggle(id: number) {
const task = tasks.value.find(t => t.id === id);
if (task) task.isDone = !task.isDone;
}

function remove(id: number) {
tasks.value = tasks.value.filter(t => t.id !== id);
}
</script>

<template>
<ul>
<TaskRow v-for="task in tasks" :key="task.id" :task="task"
@toggle="toggle" @remove="remove" />
</ul>
</template>

数据通过 props 向下流动;操作通过 emits 向上通知;父组件修改状态,再传回新的结果。连接 API 后,由父组件或组合式函数协调保存动作。

避免修改对象 prop 的内部字段

props 本身是只读的,但嵌套对象共享引用,技术上仍可能改到父组件的数据。默认不要这么做,否则状态修改来源变得隐蔽。通过事件通知数据拥有者更清楚。

组件 v-model​

Vue 3.4+ 可以用 defineModel 声明可被父组件双向绑定的接口:

src/components/TitleInput.vue
<script setup lang="ts">
const title = defineModel<string>({ required: true });
</script>

<template>
<input v-model="title" aria-label="任务名称" />
</template>

父组件先声明 const title = ref(''),再使用 <TitleInput v-model="title" />。底层约定是 modelValue prop 和 update:modelValue 事件。defineModel 把这两个部分写得更简洁。

何时拆组件​

优先提取重复出现、有独立职责或较复杂交互的区域。不要把每个 HTML 标签都拆成一个组件。任务行、任务表单和筛选栏是合理边界;“包住一个 span 的组件”通常没有必要。

组件事件不会自动像 DOM 事件一样跨层冒泡。祖父组件要接收孙组件事件,需要中间层转发,或者选择合适的共享机制。

练习​

给 TaskRow 增加 disabled?: boolean prop,在操作期间禁用 checkbox 和删除按钮。

参考答案
const props = withDefaults(defineProps<{
task: TaskItem;
disabled?: boolean;
}>(), { disabled: false });

模板两个控件都加 :disabled="props.disabled"。父组件按任务的操作状态传入该属性;不要只让按钮视觉变灰。

参考:Props、组件事件、组件 v-model。