Vue 3的“:如何处理`defineProps`与`defineEmits`?

Vue 3 <script setup>definePropsdefineEmits 的深度解析

大家好,今天我们来深入探讨 Vue 3 <script setup> 语法糖中 definePropsdefineEmits 的使用。<script setup> 极大地简化了 Vue 组件的编写,但同时也引入了一些新的概念和用法,尤其是在处理组件的 props 和 emits 时。我们会详细讲解它们的语法、类型支持、最佳实践,以及在 TypeScript 环境下的应用。

<script setup> 带来的变革

在传统的 Vue 组件中,我们需要在 export default {} 对象中声明 propsemits 选项。<script setup> 通过提供编译器宏 definePropsdefineEmits,让我们可以在 <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 的类型:

  1. 运行时声明 (Runtime Declaration): 类似于上面的例子,使用 JavaScript 对象来声明类型。这种方式的优点是兼容性好,即使在非 TypeScript 环境下也能正常工作。缺点是类型推断能力有限。

  2. 类型声明 (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:modelValuecustom-event。然后,我们使用 emit 函数来触发这些事件。

类型支持和 TypeScript:

defineProps 类似,defineEmits 也支持类型推断,尤其是在 TypeScript 环境下。我们可以使用两种方式来声明 emits 的类型:

  1. 运行时声明 (Runtime Declaration): 传递一个字符串数组给 defineEmits,声明事件名称。这种方式的优点是简单易用,兼容性好。缺点是类型信息有限。

  2. 类型声明 (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 的结合

definePropsdefineEmits 完美地融入了 Vue 3 的组合式 API。你可以在 setup 函数中使用 propsemit 来处理组件的逻辑。

例如,你可以使用 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 会在控制台中发出警告。

巧妙运用,代码更简洁

掌握 definePropsdefineEmits 的用法,能显著提升 Vue 3 组件的开发效率和代码质量。 记住,类型声明是你的好朋友,多用它能避免很多潜在的错误。 持续学习,不断探索,祝大家编码愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注