Vue 3 <script setup>
:defineProps
与 defineEmits
的深度解析
大家好,今天我们来深入探讨 Vue 3 <script setup>
语法糖中 defineProps
和 defineEmits
的使用。<script setup>
极大地简化了 Vue 组件的编写,但同时也引入了一些新的概念和用法,尤其是在处理组件的 props 和 emits 时。我们会详细讲解它们的语法、类型支持、最佳实践,以及在 TypeScript 环境下的应用。
<script setup>
带来的变革
在传统的 Vue 组件中,我们需要在 export default {}
对象中声明 props
和 emits
选项。<script setup>
通过提供编译器宏 defineProps
和 defineEmits
,让我们可以在 <script>
标签内直接声明组件的 props 和 emits,无需显式地导出组件选项。这不仅减少了代码量,也提高了代码的可读性和可维护性。
defineProps
:声明组件的 props
defineProps
用于声明组件接收的 props。它是一个编译器宏,在编译时会被 Vue 编译器处理,不会在运行时暴露给组件实例。
基本用法:
<script setup>
const props = defineProps({
message: String,
count: {
type: Number,
default: 0
},
items: {
type: Array,
required: true
}
})
console.log(props.message)
console.log(props.count)
console.log(props.items)
</script>
<template>
<div>
<p>Message: {{ props.message }}</p>
<p>Count: {{ props.count }}</p>
<ul>
<li v-for="item in props.items" :key="item">{{ item }}</li>
</ul>
</div>
</template>
在这个例子中,我们使用 defineProps
定义了三个 props:message
(String 类型),count
(Number 类型,默认值为 0),以及 items
(Array 类型,必须提供)。
类型推断和 TypeScript 支持:
defineProps
能够进行类型推断,尤其是在 TypeScript 环境下。我们可以使用两种方式来声明 props 的类型:
-
运行时声明 (Runtime Declaration): 类似于上面的例子,使用 JavaScript 对象来声明类型。这种方式的优点是兼容性好,即使在非 TypeScript 环境下也能正常工作。缺点是类型推断能力有限。
-
类型声明 (Type Declaration): 使用 TypeScript 类型来声明 props。这种方式的优点是类型推断能力强,可以提供更好的类型检查和自动补全。缺点是只能在 TypeScript 环境下使用。
类型声明的例子:
<script setup lang="ts">
interface Props {
message?: string;
count: number;
items: string[];
callback: (value: string) => void;
}
const props = defineProps<Props>();
console.log(props.message) // string | undefined
console.log(props.count) // number
console.log(props.items) // string[]
const triggerCallback = () => {
props.callback("Hello from child");
}
</script>
<template>
<div>
<p>Message: {{ props.message }}</p>
<p>Count: {{ props.count }}</p>
<ul>
<li v-for="item in props.items" :key="item">{{ item }}</li>
</ul>
<button @click="triggerCallback">Trigger Callback</button>
</div>
</template>
在这个例子中,我们定义了一个 Props
接口,描述了组件的 props 类型。然后,我们将这个接口传递给 defineProps
,告诉 Vue 编译器 props 的类型信息。注意,message
prop 使用了 ?
标记,表示它是可选的。callback
prop 展示了如何定义一个函数类型的 prop。
运行时声明与类型声明的对比:
特性 | 运行时声明 (Runtime Declaration) | 类型声明 (Type Declaration) |
---|---|---|
类型推断能力 | 弱 | 强 |
TypeScript 支持 | 部分支持 (需手动指定类型) | 完全支持 |
兼容性 | 良好 (兼容所有环境) | 仅限 TypeScript 环境 |
语法 | JavaScript 对象 | TypeScript 类型 |
默认值:
在使用类型声明时,我们需要使用 withDefaults
编译器宏来提供 props 的默认值。
<script setup lang="ts">
interface Props {
message?: string;
count: number;
items: string[];
}
const props = withDefaults(defineProps<Props>(), {
message: 'Default Message',
count: 0,
items: () => [] // 注意:数组和对象类型的默认值必须使用函数返回
});
console.log(props.message)
console.log(props.count)
console.log(props.items)
</script>
<template>
<div>
<p>Message: {{ props.message }}</p>
<p>Count: {{ props.count }}</p>
<ul>
<li v-for="item in props.items" :key="item">{{ item }}</li>
</ul>
</div>
</template>
注意,对于数组和对象类型的 props,默认值必须使用函数返回,以避免多个组件实例共享同一个默认值对象。
defineProps
的注意事项:
defineProps
只能在<script setup>
中使用。defineProps
返回的是一个只读对象,不能直接修改。- 如果同时使用运行时声明和类型声明,类型声明会覆盖运行时声明。
defineEmits
:声明组件的 emits
defineEmits
用于声明组件可以触发的自定义事件。与 defineProps
类似,它也是一个编译器宏。
基本用法:
<script setup>
const emit = defineEmits(['update:modelValue', 'custom-event'])
const updateValue = (newValue) => {
emit('update:modelValue', newValue)
}
const triggerCustomEvent = (payload) => {
emit('custom-event', payload)
}
</script>
<template>
<div>
<input type="text" @input="updateValue($event.target.value)">
<button @click="triggerCustomEvent({ message: 'Hello from child' })">Trigger Custom Event</button>
</div>
</template>
在这个例子中,我们使用 defineEmits
声明了两个事件:update:modelValue
和 custom-event
。然后,我们使用 emit
函数来触发这些事件。
类型支持和 TypeScript:
与 defineProps
类似,defineEmits
也支持类型推断,尤其是在 TypeScript 环境下。我们可以使用两种方式来声明 emits 的类型:
-
运行时声明 (Runtime Declaration): 传递一个字符串数组给
defineEmits
,声明事件名称。这种方式的优点是简单易用,兼容性好。缺点是类型信息有限。 -
类型声明 (Type Declaration): 传递一个函数类型给
defineEmits
,声明事件名称和参数类型。这种方式的优点是类型信息丰富,可以提供更好的类型检查和自动补全。缺点是只能在 TypeScript 环境下使用。
类型声明的例子:
<script setup lang="ts">
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'custom-event', payload: { message: string }): void
}>()
const updateValue = (newValue: string) => {
emit('update:modelValue', newValue)
}
const triggerCustomEvent = (payload: { message: string }) => {
emit('custom-event', payload)
}
</script>
<template>
<div>
<input type="text" @input="updateValue($event.target.value)">
<button @click="triggerCustomEvent({ message: 'Hello from child' })">Trigger Custom Event</button>
</div>
</template>
在这个例子中,我们使用了一个函数类型来声明 emits。每个函数签名代表一个事件,第一个参数是事件名称,后面的参数是事件的 payload 类型。
另一种类型声明的方式:
<script setup lang="ts">
type Emits = {
(e: 'update:modelValue', value: string): void
(e: 'custom-event', payload: { message: string }): void
}
const emit = defineEmits<Emits>()
const updateValue = (newValue: string) => {
emit('update:modelValue', newValue)
}
const triggerCustomEvent = (payload: { message: string }) => {
emit('custom-event', payload)
}
</script>
<template>
<div>
<input type="text" @input="updateValue($event.target.value)">
<button @click="triggerCustomEvent({ message: 'Hello from child' })">Trigger Custom Event</button>
</div>
</template>
这种方式先定义一个类型 Emits
,然后再传递给 defineEmits
,代码可读性更好。
运行时声明与类型声明的对比:
特性 | 运行时声明 (Runtime Declaration) | 类型声明 (Type Declaration) |
---|---|---|
类型推断能力 | 弱 | 强 |
TypeScript 支持 | 部分支持 (需手动指定类型) | 完全支持 |
兼容性 | 良好 (兼容所有环境) | 仅限 TypeScript 环境 |
语法 | 字符串数组 | 函数类型 |
defineEmits
的注意事项:
defineEmits
只能在<script setup>
中使用。defineEmits
返回的是一个 emit 函数,用于触发事件。- 如果同时使用运行时声明和类型声明,类型声明会覆盖运行时声明。
- 强烈建议使用类型声明,以获得更好的类型安全性和代码提示。
v-model
的简化:
<script setup>
简化了 v-model
的使用。如果你的 prop 名是 modelValue
,并且 emit 的事件名是 update:modelValue
,那么 Vue 会自动将它们关联起来,你就可以直接使用 v-model
指令。
<script setup lang="ts">
interface Props {
modelValue: string;
}
const props = defineProps<Props>();
const emit = defineEmits(['update:modelValue']);
const updateValue = (event: Event) => {
const target = event.target as HTMLInputElement;
emit('update:modelValue', target.value);
};
</script>
<template>
<input type="text" :value="props.modelValue" @input="updateValue" />
</template>
在父组件中,你可以这样使用:
<template>
<MyInput v-model="myValue" />
<p>Value: {{ myValue }}</p>
</template>
<script setup>
import { ref } from 'vue';
import MyInput from './MyInput.vue';
const myValue = ref('');
</script>
组合式 API 与 defineProps
/defineEmits
的结合
defineProps
和 defineEmits
完美地融入了 Vue 3 的组合式 API。你可以在 setup
函数中使用 props
和 emit
来处理组件的逻辑。
例如,你可以使用 watch
来监听 props 的变化:
<script setup lang="ts">
import { watch } from 'vue';
interface Props {
count: number;
}
const props = defineProps<Props>();
watch(
() => props.count,
(newCount, oldCount) => {
console.log(`Count changed from ${oldCount} to ${newCount}`);
}
);
</script>
<template>
<div>Count: {{ props.count }}</div>
</template>
最佳实践总结
- 优先使用类型声明 (Type Declaration): 在 TypeScript 环境下,尽可能使用类型声明来定义 props 和 emits,以获得更好的类型安全性和代码提示。
- 使用
withDefaults
提供默认值: 当使用类型声明定义 props 时,使用withDefaults
编译器宏来提供 props 的默认值。对于数组和对象类型的 props,默认值必须使用函数返回。 - 保持事件名称的一致性: 对于
v-model
,确保 prop 名是modelValue
,并且 emit 的事件名是update:modelValue
。 - 清晰地定义事件 payload 类型: 在类型声明中,清晰地定义事件 payload 的类型,以避免运行时错误。
- 使用常量来定义事件名称: 如果你的组件触发了很多自定义事件,可以考虑使用常量来定义事件名称,以提高代码的可读性和可维护性。
实际案例分析
假设我们正在开发一个自定义的 select 组件。我们需要定义一个 options
prop,用于显示下拉选项,以及一个 modelValue
prop,用于绑定选中的值。同时,我们需要 emit 一个 update:modelValue
事件,当用户选择不同的选项时,更新父组件的值。
// MySelect.vue
<script setup lang="ts">
interface Props {
options: { value: string; label: string }[];
modelValue: string;
}
const props = defineProps<Props>();
const emit = defineEmits(['update:modelValue']);
const handleChange = (event: Event) => {
const target = event.target as HTMLSelectElement;
emit('update:modelValue', target.value);
};
</script>
<template>
<select :value="props.modelValue" @change="handleChange">
<option v-for="option in props.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</template>
在父组件中,我们可以这样使用:
// ParentComponent.vue
<template>
<MySelect v-model="selectedValue" :options="options" />
<p>Selected Value: {{ selectedValue }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import MySelect from './MySelect.vue';
const selectedValue = ref('');
const options = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'orange', label: 'Orange' },
];
</script>
高级用法:Prop 验证
defineProps
允许你自定义 prop 验证逻辑。这在某些情况下非常有用,例如,你需要验证 prop 的值是否符合特定的格式。
<script setup>
import { defineProps } from 'vue';
defineProps({
email: {
type: String,
validator: (value) => {
// 简单的邮箱格式验证
return /^[^s@]+@[^s@]+.[^s@]+$/.test(value);
},
},
});
</script>
如果 email
prop 的值不符合验证规则,Vue 会在控制台中发出警告。
巧妙运用,代码更简洁
掌握 defineProps
和 defineEmits
的用法,能显著提升 Vue 3 组件的开发效率和代码质量。 记住,类型声明是你的好朋友,多用它能避免很多潜在的错误。 持续学习,不断探索,祝大家编码愉快!