defineProps() 和 defineEmits()
为了在声明 props
和 emits
选项时获得完整的类型推导支持,我们可以使用 defineProps
和 defineEmits
API,它们将自动地在 <script setup>
中可用:
html
<template>
<div>
<h1>{{ props.foo }}</h1>
<button @click="handleChange">Change</button>
<button @click="handleDelete">Delete</button>
</div>
</template>
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
function handleChange() {
emit('change', props.foo)
}
function handleDelete() {
emit('delete')
}
</script>
defineProps
和defineEmits
都是只能在<script setup>
中使用的编译器宏。他们不需要导入,且会随着<script setup>
的处理过程一同被编译掉。defineProps
接收与props
选项相同的值,defineEmits
接收与emits
选项相同的值。defineProps
和defineEmits
在选项传入后,会提供恰当的类型推导。
针对TS类型的 props/emit 声明
props
和 emit
也可以通过给 defineProps
和 defineEmits
传递纯TS类型参数的方式来声明:
typescript
const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string, status: boolean): void
}>()
// 3.3+:另一种更简洁的语法
const emit = defineEmits<{
change: [id: number] // 具名元组语法
update: [value: string, status: boolean]
}>()
-
defineProps
或defineEmits
要么使用运行时声明 ,要么使用类型声明。同时使用两种声明方式会导致编译报错。-
示例 1: 使用运行时声明
jsconst props = defineProps({ foo: String, bar: Number }) const emit = defineEmits(['change', 'delete'])
-
示例 2: 使用类型声明
jsconst props = defineProps<{ foo: string bar?: number }>() const emit = defineEmits<{ (e: 'change', id: number): void (e: 'delete'): void }>()
-
错误示例: 同时使用运行时声明和类型声明
js// ❌ 会导致编译报错 const props = defineProps<{ foo: string }>({ bar: Number })
typescript// ❌ 会导致编译报错 const emit = defineEmits<{ (e: 'change', id: number): void }>(['delete'])
-
-
使用类型声明的时候,静态分析会自动生成等效的运行时声明,从而在避免双重声明的前提下确保正确的运行时行为。
- 在开发模式下,编译器会试着从类型来推导对应的运行时验证。例如这里从
foo: string
类型中推断出foo: String
。如果类型是对导入类型的引用,这里的推导结果会是foo: null
(与any
类型相等),因为编译器没有外部文件的信息。 - 在生产模式下,编译器会生成数组格式的声明来减少打包体积 (这里的 props 会被编译成
['foo', 'bar']
)。
- 在开发模式下,编译器会试着从类型来推导对应的运行时验证。例如这里从
在 Vue 3.2 及以下版本中,defineProps()
的泛型类型参数只能使用类型字面量或本地接口的引用:
typescript
interface Props {
foo: string
bar?: number
}
const props = defineProps<Props>()
在 Vue 3.3 及以上版本中,可以在类型参数的位置引用导入的类型或有限的复杂类型:
typescript
import { SomeType } from './types'
const props = defineProps<SomeType>()
然而,仍然无法使用需要实际类型分析的复杂类型,例如条件类型:
typescript
// ❌ 不支持
type ConditionalProps<T> = T extends string ? { foo: T } : { bar: T }
const props = defineProps<ConditionalProps<number>>()
但可以在单个 prop 的类型上使用条件类型:
typescript
// ✅ 支持
const props = defineProps<{
foo: string | number
bar: string extends 'test' ? number : boolean
}>()
响应式 Props 解构
在 Vue 3.5 及以上版本中,从 defineProps
返回值解构出的变量是响应式的。当在同一个 <script setup>
块中的代码访问从 defineProps
解构出的变量时,Vue 的编译器会自动在前面添加 props.
。
js
const { foo } = defineProps(['foo'])
watchEffect(() => {
// 在 3.5 之前仅运行一次
// 在 3.5+ 版本中会在 "foo" prop 改变时重新运行
console.log(foo)
})
以上编译成以下等效内容:
js
const props = defineProps(['foo'])
watchEffect(() => {
// `foo` 由编译器转换为 `props.foo`
console.log(props.foo)
})
此外,你可以使用 JavaScript 原生的默认值语法声明 props 的默认值。这在使用基于类型的 props 声明时特别有用。
js
interface Props {
msg?: string
labels?: string[]
}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()
使用类型声明时的默认 props 值
在 3.5 及以上版本中,当使用响应式 Props
解构时,可以自然地声明默认值。但在 3.4 及以下版本中,默认情况下并未启用响应式 Props
解构。为了用基于类型声明的方式声明 props
的默认值,需要使用 withDefaults
编译器宏:
js
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})
上面代码会被编译为等价的运行时 props
的 default
选项。此外,withDefaults
辅助函数提供了对默认值的类型检查,并确保返回的 props
的类型删除了已声明默认值的属性的可选标志。
在使用 withDefaults
时,如果默认值是可变引用类型(如数组或对象),应将其封装在函数中,以避免意外修改和外部副作用。以下是一个示例:
js
interface Props {
items?: string[]
config?: {
theme: string
layout: string
}
}
const props = withDefaults(defineProps<Props>(), {
items: () => ['item1', 'item2', 'item3'], // 使用函数返回数组
config: () => ({ theme: 'dark', layout: 'grid' }) // 使用函数返回对象
})
在上述代码中,items
和 config
的默认值是通过函数返回的。这确保了每个组件实例都能获得独立的默认值副本,而不会因为共享同一个引用而导致意外的修改。
- 错误示例
如果直接使用对象或数组作为默认值,可能会导致所有组件实例共享同一个引用,从而引发意外行为:
js
const props = withDefaults(defineProps<Props>(), {
items: ['item1', 'item2', 'item3'], // ❌ 直接使用数组
config: { theme: 'dark', layout: 'grid' } // ❌ 直接使用对象
})
在这种情况下,修改一个组件实例的 items
或 config
会影响其他实例的值,这是不符合预期的。
- 使用默认值解构
当使用默认值解构时,不需要封装在函数中,因为解构操作会为每个实例创建独立的值:
js
const { items = ['item1', 'item2', 'item3'], config = { theme: 'dark', layout: 'grid' } } = defineProps<Props>()
这种方式同样可以确保每个组件实例获得自己的默认值副本。
defineModel()
这个宏可以用来声明一个双向绑定 prop
,通过父组件的 v-model
来使用。组件 v-model
指南中也讨论了示例用法。
在底层,这个宏声明了一个 model
prop
和一个相应的值更新事件 。如果第一个参数是一个字符串字面量,它将被用作 prop
名称;否则,prop
名称将默认为 modelValue
。在这两种情况下,你都可以再传递一个额外的对象,它可以包含 prop
的选项和 model
ref
的值转换选项。
js
// 声明 "modelValue" prop,由父组件通过 v-model 使用
const model = defineModel()
// 或者:声明带选项的 "modelValue" prop
const model = defineModel({ type: String })
// 在被修改时,触发 "update:modelValue" 事件
model.value = "hello"
// 声明 "count" prop,由父组件通过 v-model:count 使用
const count = defineModel("count")
// 或者:声明带选项的 "count" prop
const count = defineModel("count", { type: Number, default: 0 })
function inc() {
// 在被修改时,触发 "update:count" 事件
count.value++
}
在实际项目中,子组件可以用 defineModel
实现双向绑定:
// 子组件 CInput.vue
html
<template>
<input v-model="modelValue" placeholder="输入内容" />
</template>
<script setup lang="ts">
const modelValue = defineModel<string>({ default: '' })
</script>
// 父组件
html
<template>
<c-input v-model="inputval" />
<div>{{ inputval }}</div>
</template>
<script setup>
import { ref } from 'vue';
import CInput from './components/CInput.vue';
const inputval = ref('')
</script>
WARNING
如果为 defineModel
prop
设置了一个 default
值且父组件没有为该 prop 提供任何值,会导致父组件与子组件之间不同步。在下面的示例中,父组件的 myRef
是 undefined,而子组件的 model
是 1:
js
// 子组件:
const model = defineModel({ default: 1 })
// 父组件
const myRef = ref()
js
<Child v-model="myRef"></Child>
解决方法:在父组件初始化时为 v-model
绑定的变量设置默认值,使其与子组件的默认值保持一致。
js
// 父组件
const myRef = ref(1) // 初始化为与子组件默认值一致
修饰符和转换器
为了获取 v-model
指令使用的修饰符,我们可以像这样解构 defineModel()
的返回值:
// 父组件
html
<c-input v-model.trim="inputval" />
// 子组件
js
const [modelValue, modelModifiers] = defineModel()
// 对应 v-model.trim
if (modelModifiers.trim) {
// ...
}
当存在修饰符时,我们可能需要在读取或将其同步回父组件时对其值进行转换。我们可以通过使用 get
和 set
转换器选项来实现这一点:
// 子组件
js
const [modelValue, modelModifiers] = defineModel({
// get() 省略了,因为这里不需要它
set(value) {
// 如果使用了 .trim 修饰符,则返回裁剪过后的值
if (modelModifiers.trim) {
return value.trim()
}
// 否则,原样返回
return value
}
})
在 TypeScript 中使用
与 defineProps
和 defineEmits
一样,defineModel
也可以接收类型参数来指定 model 值和修饰符的类型:
defineModel<string>()
typescript
const modelValue = defineModel<string>()
// ^? Ref<string | undefined>
defineModel<string>()
定义了一个v-model
,其类型为string
。- 返回值是一个
Ref<string | undefined>
,表示modelValue
是一个响应式引用,可能是string
类型,也可能是undefined
。 - 默认情况下,
v-model
的值是可选的,因此会包含undefined
。
defineModel<string>({ required: true })
typescript
const modelValue = defineModel<string>({ required: true })
// ^? Ref<string>
- 这里通过传递选项
{ required: true }
,将v-model
的值设置为必填。 - 这意味着
modelValue
不再可能是undefined
,它的类型变为Ref<string>
。 - 这种用法适合需要确保
v-model
始终有值的场景。
defineModel<string, "trim" | "uppercase">()
typescript
const [modelValue, modifiers] = defineModel<string, "trim" | "uppercase">()
// ^? Record<'trim' | 'uppercase', true | undefined>
- 这里定义了一个带有修饰符的
v-model
,例如v-model.trim
或v-model.uppercase
。 - 返回值是一个数组:
modelValue
是Ref<string | undefined>
,表示响应式的模型值。modifiers
是一个对象,类型为Record<'trim' | 'uppercase', true | undefined>
,表示修饰符的状态。- 如果某个修饰符被使用(如
v-model.trim
),对应的值为true
。 - 如果未使用,则为
undefined
。
- 如果某个修饰符被使用(如
Record 语法
Record
是 TypeScript 中的一个实用类型(Utility Type),用于构造一个对象类型,其键和值的类型都可以被明确指定。
Record
通常用于:
- 定义固定键值对的对象类型。
- 动态生成对象类型,避免手动重复定义。
typescript
Record<Keys, Type>
Keys
: 对象的键的类型,通常是字符串字面量类型或联合类型。Type
: 对象的值的类型。
typescript
Record<'trim' | 'uppercase', true | undefined>
这段代码的含义是:创建一个对象类型,该对象的键是 trim
或 uppercase
,值是 true
或 undefined
。
等价于:
typescript
{
trim: true | undefined;
uppercase: true | undefined;
}
typescript
const options: Record<'trim' | 'uppercase', true | undefined> = {
trim: true,
uppercase: undefined,
};
在这个例子中,options
是一个对象,必须包含 trim
或 uppercase
,并且它们的值只能是 true
或 undefined
。
defineExpose()
使用 <script setup>
的组件是默认关闭 的------即通过模板引用或者 $parent
链获取到的组件的公开实例,不会暴露任何在 <script setup>
中声明的绑定。
可以通过 defineExpose
编译器宏来显式指定在 <script setup>
组件中要暴露出去的属性:
js
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
当父组件通过模板引用的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number }
(ref
会和在普通实例中一样被自动解包)
以下是一个关于如何使用 defineExpose
的实例讲解,展示如何在 <script setup>
中显式暴露属性,以便外部组件可以访问这些属性。
示例代码
-
父组件 (ParentComponent.vue)
html<template> <ChildComponent ref="childRef" /> <button @click="callChildMethod">调用子组件方法</button> </template> <script setup> import ChildComponent from './ChildComponent.vue'; const childRef = useTemplateRef('childRef'); function callChildMethod() { if (childRef.value) { childRef.value.exposedMethod(); // 调用子组件暴露的方法 } } </script>
-
子组件 (ChildComponent.vue)
vue<template> <div>子组件内容</div> </template> <script setup> import { defineExpose } from 'vue'; function exposedMethod() { console.log('子组件方法被调用'); } // 使用 defineExpose 显式暴露方法 defineExpose({ exposedMethod, }); </script>
defineSlots()
defineSlots
是 Vue 3 <script setup>
的编译宏,用于类型化插槽,让 TypeScript 能够对插槽名称和插槽 props 进行类型检查和智能提示。它只在编译阶段生效,不会影响运行时。
- 类型安全:确保插槽名称和 props 类型正确,减少运行时错误。
- IDE 提示:在编辑器中获得插槽相关的自动补全和类型提示。
基本语法如下
typescript
<script setup lang="ts">
const slots = defineSlots<{
default(props: { msg: string }): any
}>()
// slots.default({ msg: 'hello' }) // 类型检查
</script>
- 泛型参数是一个对象,键为插槽名称,值为函数类型。
- 函数的参数是插槽 props 的类型,返回值类型通常用
any
。
多插槽示例
假设有一个组件支持多个插槽:
typescript
<script setup lang="ts">
const slots = defineSlots<{
header(props: { title: string }): any
default(props: { msg: string }): any
footer(props: { count: number }): any
}>()
</script>
header
插槽要求title
为字符串。default
插槽要求msg
为字符串。footer
插槽要求count
为数字。
父组件如何传递插槽
html
<template>
<Child class="child-style" expand>
<template #header="{ title }">
<div>header部分描述:{{ title }}</div>
</template>
<template #default="{ count }">
<p>default部分描述:{{ count }}</p>
</template>
<template #footer="{ msg }">
<div>footer部分描述:{{ msg }}</div>
</template>
</Child>
</template>
<script setup>
import Child from './components/Child.vue';
</script>
- 父组件传递的插槽参数会被类型检查,确保类型正确。
子组件如何传值给父组件
html
<template>
<slot name="header" headerDesc="顶部部分描述"></slot>
<slot defaultDesc="插槽描述">默认插槽描述</slot>
<slot name="footer" footerDesc="底部部分描述"></slot>
</template>
<script setup lang="ts">
const slots = defineSlots<{
header(props: { headerDesc: string }):any
footer( props: { footerDesc: string } ): any
default( props: { defaultDesc: string } ): any
}>()
</script>
类型推断和 IDE 提示
在 <script setup>
中,使用 slots.header({ title: 'Hello' })
时,IDE 会自动提示 title
必须是字符串,传递错误类型会报错。
可选插槽和返回值类型
你可以将插槽定义为可选:
typescript
<script setup lang="ts">
const slots = defineSlots<{
header?(props: { title: string }): any
default(props: { msg: string }): any
}>()
</script>
header?
表示header
插槽是可选的。
返回值类型可以更具体:
typescript
<script setup lang="ts">
const slots = defineSlots<{
default(props: { msg: string }): VNode[]
}>()
</script>
useSlots() 和 useAttrs()
在 <script setup>
中使用 slots
和 attrs
的情况相对较少,因为可以直接通过模板中的 $slots
和 $attrs
访问它们。然而,在某些特定场景下,仍然可以使用 useSlots
和 useAttrs
辅助函数来获取插槽和属性。
以下是一个实际使用 useSlots
和 useAttrs
的示例:
-
子组件 (ChildComponent.vue)
html<template> <div v-bind="attrs"> <slot name="header" /> <p>子组件内容</p> <slot /> </div> </template> <script setup> import { useSlots, useAttrs } from 'vue' const slots = useSlots() const attrs = useAttrs() // 检查是否提供了名为 "header" 的插槽 if (!slots.header) { console.warn('未提供 header 插槽') } </script>
-
父组件 (ParentComponent.vue)
html<template> <ChildComponent class="custom-class"> <template #header> <h1>这是标题插槽内容</h1> </template> <p>这是默认插槽内容</p> </ChildComponent> </template> <script setup> import ChildComponent from './ChildComponent.vue' </script>
说明:
-
useSlots
:- 在子组件中使用
useSlots
检查是否提供了特定的插槽(如header
)。 - 如果未提供插槽,输出警告信息。
- 在子组件中使用
-
useAttrs
:- 使用
useAttrs
获取父组件传递的属性(如class="custom-class"
)。 - 使用
v-bind="attrs"
将这些属性绑定到子组件的根元素。
- 使用
-
父组件:
- 通过
#header
提供了一个具名插槽。 - 默认插槽内容直接放置在
<ChildComponent>
标签内。
- 通过
运行结果:
- 子组件会渲染标题插槽内容、默认插槽内容,并应用父组件传递的
class
属性。 - 如果父组件未提供
header
插槽,子组件会在控制台输出警告信息。
泛型
可以使用 <script>
标签上的 generic
属性声明泛型类型参数:
js
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
selected: T
}>()
</script>
generic
的值与 TypeScript 中位于 <...>
之间的参数列表完全相同。例如,你可以使用多个参数,extends
约束,默认类型和引用导入的类型:
js
<script
setup
lang="ts"
generic="T extends string | number, U extends Item"
>
import type { Item } from './types'
defineProps<{
id: T
list: U[]
}>()
</script>
为了在 ref
中使用泛型组件的引用,你需要使用 vue-component-type-helpers
库,因为 InstanceType
在这种场景下不起作用。
typescript
<script
setup
lang="ts"
>
import componentWithoutGenerics from '../component-without-generics.vue'; // 一个没有泛型的普通组件。
import genericComponent from '../generic-component.vue'; // 一个带有泛型的组件。
import type { ComponentExposed } from 'vue-component-type-helpers';
// 适用于没有泛型的组件
ref<InstanceType<typeof componentWithoutGenerics>>();
// 适用于有泛型的组件
ref<ComponentExposed<typeof genericComponent>>();
这段代码展示了如何在 Vue 3 的 <script setup>
中使用 TypeScript 来处理组件的引用,尤其是泛型组件的引用。以下是逐步的详细解释:
- 导入类型工具
typescript
import type { ComponentExposed } from 'vue-component-type-helpers';
- 从
vue-component-type-helpers
库中导入了ComponentExposed
类型工具。 ComponentExposed
是一个辅助类型,用于提取组件的暴露类型(即组件实例的类型),特别适用于泛型组件。
- 处理没有泛型的组件引用
typescript
ref<InstanceType<typeof componentWithoutGenerics>>();
ref
是 Vue 的响应式 API,用于创建一个响应式引用。InstanceType<typeof componentWithoutGenerics>
:InstanceType
是 TypeScript 的内置工具类型,用于获取构造函数的实例类型。typeof componentWithoutGenerics
获取组件的类型。- 结合起来,
InstanceType<typeof componentWithoutGenerics>
表示componentWithoutGenerics
的实例类型。
- 适用于没有泛型的普通组件。
- 处理带有泛型的组件引用
typescript
ref<ComponentExposed<typeof genericComponent>>();
ref<ComponentExposed<typeof genericComponent>>
:ComponentExposed
是从vue-component-type-helpers
导入的类型工具。typeof genericComponent
获取组件的类型。ComponentExposed<typeof genericComponent>
提取了genericComponent
的暴露类型。
- 适用于带有泛型的组件,因为
InstanceType
无法正确处理泛型组件的类型。
总结:
- 普通组件 :可以直接使用
InstanceType
获取实例类型。 - 泛型组件 :需要借助
vue-component-type-helpers
提供的ComponentExposed
类型工具,因为InstanceType
无法正确处理泛型。
这种方式确保了在使用组件引用时,能够获得正确的类型推断和静态检查,提升了代码的安全性和可维护性。
为了更好地理解没有泛型的普通组件和带有泛型的组件的使用场景,以下是具体的示例讲解:
- 没有泛型的普通组件
假设我们有一个普通的 Vue 组件 ButtonComponent.vue
,它不使用任何泛型:
html
<!-- filepath: ButtonComponent.vue -->
<template>
<button>{{ label }}</button>
</template>
<script setup lang="ts">
defineProps<{
label: string
}>();
</script>
在 <script setup>
中,我们可以通过 ref
和 InstanceType
来引用该组件的实例:
typescript
<script setup lang="ts">
import ButtonComponent from './ButtonComponent.vue';
const buttonRef = ref<InstanceType<typeof ButtonComponent>>();
</script>
这里的 InstanceType<typeof ButtonComponent>
提供了 ButtonComponent
的实例类型,适用于没有泛型的普通组件。
- 带有泛型的组件
假设我们有一个带有泛型的 Vue 组件 GenericList.vue
,它接受一个泛型 T
来定义列表项的类型:
html
<!-- filepath: GenericList.vue -->
<template>
<ul>
<li v-for="(item, index) in items" :key="index">{{ item }}</li>
</ul>
</template>
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
}>();
</script>
在 <script setup>
中,我们需要使用 vue-component-type-helpers
提供的 ComponentExposed
来正确引用该组件的实例:
typescript
<script setup lang="ts">
import GenericList from './GenericList.vue';
import type { ComponentExposed } from 'vue-component-type-helpers';
const listRef = ref<ComponentExposed<typeof GenericList>>();
</script>
这里的 ComponentExposed<typeof GenericList>
提取了 GenericList
的暴露类型,适用于带有泛型的组件,因为 InstanceType
无法正确处理泛型。
通过这些示例,我们可以清楚地看到如何在 Vue 3 的 <script setup>
中使用 TypeScript 来处理组件的引用,尤其是泛型组件的引用。
在 Vue 3.4 及以上版本,推荐使用 useTemplateRef
进行子组件引用,而不是直接用 ref
。useTemplateRef
能更好地处理类型推断,尤其是泛型组件。
typescript
<script setup lang="ts">
import componentWithoutGenerics from '../component-without-generics.vue'; // 没有泛型的组件
import genericComponent from '../generic-component.vue'; // 带泛型的组件
import type { ComponentExposed } from 'vue-component-type-helpers';
// 没有泛型的组件引用
const normalRef = useTemplateRef<InstanceType<typeof componentWithoutGenerics>>('normalRef');
// 带泛型的组件引用
const genericRef = useTemplateRef<ComponentExposed<typeof genericComponent>>('genericRef');
</script>
这样可以确保在 <script setup>
中获得正确的类型推断和 IDE 智能提示,无论是普通组件还是泛型组件。
限制
以下是一个关于 <script setup>
的实例讲解,帮助你理解其用法和限制。
假设我们有一个 Vue 3 组件,使用 <script setup>
来定义逻辑和模板。
html
<template>
<div>
<h1>{{ message }}</h1>
<button @click="increment">点击次数: {{ count }}</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 定义响应式数据
const message = 'Hello, Vue 3!';
const count = ref(0);
// 定义方法
const increment = () => {
count.value++;
};
</script>
限制说明:
-
不能与 src 属性一起使用
如果你尝试将
<script setup>
的逻辑提取到外部文件并通过 src 引入,会导致上下文丢失。例如:vue<script setup src="./logic.ts"></script>
这种写法是不支持的,因为
<script setup>
的代码依赖于单文件组件的上下文。 -
不支持 DOM 内根组件模板
<script setup>
不支持直接在 DOM 内使用根组件模板。例如:html<div id="app"> <MyComponent /> </div>
如果
MyComponent
使用了<script setup>
,它必须通过 Vue 的createApp
挂载,而不能直接在 DOM 内使用。