尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

组件类型-Props-Emits-Ref

组件类型-Props-Emits-Ref
📅 发布时间:2026/7/3 4:32:05

文章目录

  • 前言
  • 一、Props 类型声明
    • 1.1 运行时对象写法
    • 1.2 泛型写法
    • 1.3 withDefaults 默认值
    • 1.4 Props 类型导出给父组件
  • 二、Props 解构与响应性
    • 2.1 不建议直接普通解构
    • 2.2 使用 toRefs 保留响应性
  • 三、Emits 类型声明
    • 3.1 数组写法
    • 3.2 函数重载写法
    • 3.3 命名元组写法
    • 3.4 父组件监听事件
  • 四、v-model 的类型
    • 4.1 modelValue + update:modelValue
    • 4.2 多个 v-model
  • 五、模板 Ref 类型
    • 5.1 DOM ref
    • 5.2 组件 ref
  • 六、defineExpose 暴露方法类型
  • 七、Slot 类型简单了解
  • 八、面试聚焦
    • 8.1 defineProps 泛型写法为何推荐?
    • 8.2 Props 解构会不会丢失响应性?
    • 8.3 defineEmits 如何限制参数?
    • 8.4 模板 ref 为什么要写 null?
  • 九、易混淆点
  • 十、思考与练习
  • 总结

前言

在 Vue 3 + TypeScript 项目中,组件类型是最常写、也最容易踩坑的一块。Props 写得不清楚,父组件传参容易出错;Emits 没有约束,事件名和参数容易写散;模板 ref 没有类型,调用 DOM 或子组件方法时经常只能any或非空断言硬顶。

本篇围绕组件开发中最高频的三类类型展开:

  • defineProps:父传子参数如何声明类型与默认值
  • defineEmits:子传父事件如何约束事件名与参数
  • ref/ 模板 ref:如何拿到 DOM、组件实例与暴露方法的类型

一、Props 类型声明

1.1 运行时对象写法

Vue 仍支持传统运行时对象写法,适合需要运行时校验、默认值、必填项的场景:

<script setup lang="ts"> const props = defineProps({ title: { type: String, required: true }, count: { type: Number, default: 0 }, disabled: { type: Boolean, default: false } }) console.log(props.title) </script>

这种写法的优点是保留 Vue 的运行时 props 校验;缺点是复杂对象、联合类型、字面量类型表达起来不够自然。


1.2 泛型写法

在<script setup lang="ts">中,更推荐用泛型声明 Props:

<script setup lang="ts"> interface UserCardProps { id: number name: string avatar?: string role: 'admin' | 'user' | 'guest' } const props = defineProps<UserCardProps>() console.log(props.name) </script>

泛型写法更接近 TS 的类型系统,适合:

  • 复杂对象类型
  • 联合类型
  • 字面量类型
  • 从其他文件导入类型
  • 组件之间复用 Props 类型
// types/user.tsexportinterfaceUser{id:numbername:stringemail?:string}
<script setup lang="ts"> import type { User } from '@/types/user' defineProps<{ user: User size?: 'small' | 'medium' | 'large' }>() </script>

注意:import type只导入类型,编译后不会进入运行时代码,推荐在 TS 项目中养成这个习惯。


1.3 withDefaults 默认值

泛型写法没有运行时对象里的default字段,如果要设置默认值,需要配合withDefaults:

<script setup lang="ts"> interface Props { title: string count?: number size?: 'small' | 'medium' | 'large' tags?: string[] } const props = withDefaults(defineProps<Props>(), { count: 0, size: 'medium', tags: () => [] }) </script>

这里有两个重点:

  1. count?、size?表示父组件可以不传。
  2. 数组、对象默认值建议用函数返回,避免引用共享。

withDefaults后,组件内部读取props.count时,TS 会知道它已经有默认值,不再是number | undefined。


1.4 Props 类型导出给父组件

如果某个组件的 Props 会被父组件、配置项或测试复用,可以导出类型:

<script setup lang="ts"> export interface UserCardProps { id: number name: string role?: 'admin' | 'user' } withDefaults(defineProps<UserCardProps>(), { role: 'user' }) </script>

父组件或其他模块可以复用:

importtype{UserCardProps}from'@/components/UserCard.vue'constdefaultUser:UserCardProps={id:1,name:'张三',role:'admin'}

这种写法在中后台项目中很实用,尤其是表格列配置、弹窗表单配置和组件测试。


二、Props 解构与响应性

2.1 不建议直接普通解构

defineProps返回的是响应式 props 对象。普通解构在一些写法中容易丢失响应性:

<script setup lang="ts"> const props = defineProps<{ keyword: string }>() // 推荐:通过 props.keyword 使用 console.log(props.keyword) </script>

如果你需要在逻辑里频繁使用某个字段,优先保持props.xxx,语义清楚,也不容易误判响应性。

2.2 使用 toRefs 保留响应性

需要解构时,可以用toRefs:

<script setup lang="ts"> import { toRefs, watch } from 'vue' const props = defineProps<{ keyword: string page: number }>() const { keyword, page } = toRefs(props) watch(keyword, (val) => { console.log('keyword changed:', val) }) console.log(page.value) </script>

toRefs(props)得到的是Ref,所以在<script>中需要.value,模板中自动解包。


三、Emits 类型声明

3.1 数组写法

最简单的写法只限制事件名:

<script setup lang="ts"> const emit = defineEmits(['close', 'submit']) emit('close') emit('submit') </script>

这种写法能限制事件名,但不能限制事件参数。比如submit到底要不要传表单数据,TS 并不知道。


3.2 函数重载写法

传统类型写法可以用函数重载描述不同事件:

<script setup lang="ts"> interface FormData { username: string password: string } const emit = defineEmits<{ (e: 'close'): void (e: 'submit', data: FormData): void (e: 'change', value: string | number): void }>() emit('close') emit('submit', { username: 'admin', password: '123456' }) emit('change', 'enabled') </script>

优点是兼容性好,很多老项目和库类型里都能看到这种写法。


3.3 命名元组写法

Vue 3.3+ 支持更简洁的写法:

<script setup lang="ts"> interface FormData { username: string password: string } const emit = defineEmits<{ close: [] submit: [data: FormData] change: [value: string | number] }>() emit('close') emit('submit', { username: 'admin', password: '123456' }) emit('change', 1) </script>

这类写法更像“事件名 → 参数列表”的映射,读起来直观,也适合团队统一规范。


3.4 父组件监听事件

子组件:

<!-- UserForm.vue --> <script setup lang="ts"> interface UserForm { name: string age: number } const emit = defineEmits<{ submit: [form: UserForm] cancel: [] }>() const onSubmit = () => { emit('submit', { name: '张三', age: 18 }) } </script> <template> <button @click="onSubmit">提交</button> <button @click="emit('cancel')">取消</button> </template>

父组件:

<script setup lang="ts"> import UserForm from './UserForm.vue' const handleSubmit = (form: { name: string; age: number }) => { console.log(form.name, form.age) } </script> <template> <UserForm @submit="handleSubmit" @cancel="console.log('cancel')" /> </template>

在 IDE 中,事件名、参数数量、参数类型都能得到提示。


四、v-model 的类型

4.1 modelValue + update:modelValue

Vue 3 中,组件上的v-model本质是:

  • 父传子:modelValue
  • 子传父:update:modelValue
<!-- BaseInput.vue --> <script setup lang="ts"> defineProps<{ modelValue: string }>() const emit = defineEmits<{ 'update:modelValue': [value: string] }>() </script> <template> <input :value="modelValue" @input="emit('update:modelValue', ($event.target as HTMLInputElement).value)" /> </template>

父组件:

<script setup lang="ts"> import { ref } from 'vue' import BaseInput from './BaseInput.vue' const keyword = ref('') </script> <template> <BaseInput v-model="keyword" /> </template>

这里modelValue是string,所以父组件的keyword也应是Ref<string>。


4.2 多个 v-model

多个v-model会变成不同的 prop 与事件:

<!-- SearchPanel.vue --> <script setup lang="ts"> defineProps<{ keyword: string page: number }>() const emit = defineEmits<{ 'update:keyword': [value: string] 'update:page': [value: number] }>() </script> <template> <input :value="keyword" @input="emit('update:keyword', ($event.target as HTMLInputElement).value)" /> <button @click="emit('update:page', page + 1)">下一页</button> </template>

父组件:

<template> <SearchPanel v-model:keyword="keyword" v-model:page="page" /> </template>

五、模板 Ref 类型

5.1 DOM ref

访问 DOM 节点时,要把初始值写成null,并显式标注元素类型:

<script setup lang="ts"> import { onMounted, ref } from 'vue' const inputRef = ref<HTMLInputElement | null>(null) onMounted(() => { inputRef.value?.focus() }) </script> <template> <input ref="inputRef" /> </template>

常见 DOM 类型:

元素类型
inputHTMLInputElement
textareaHTMLTextAreaElement
selectHTMLSelectElement
divHTMLDivElement
formHTMLFormElement

口诀:模板 ref 初始化为null,使用时可选链?.。


5.2 组件 ref

如果 ref 指向子组件,可以用InstanceType<typeof Comp>:

<script setup lang="ts"> import { ref, onMounted } from 'vue' import UserDialog from './UserDialog.vue' const dialogRef = ref<InstanceType<typeof UserDialog> | null>(null) onMounted(() => { dialogRef.value?.open() }) </script> <template> <UserDialog ref="dialogRef" /> </template>

这要求子组件通过defineExpose暴露方法,否则父组件拿不到。


六、defineExpose 暴露方法类型

子组件默认是关闭的,父组件不能随便访问内部变量。需要暴露给父组件的方法,用defineExpose:

<!-- UserDialog.vue --> <script setup lang="ts"> import { ref } from 'vue' const visible = ref(false) const open = () => { visible.value = true } const close = () => { visible.value = false } defineExpose({ open, close }) </script> <template> <div v-if="visible">用户弹窗</div> </template>

父组件:

<script setup lang="ts"> import { ref } from 'vue' import UserDialog from './UserDialog.vue' const dialogRef = ref<InstanceType<typeof UserDialog> | null>(null) const showDialog = () => { dialogRef.value?.open() } </script> <template> <button @click="showDialog">打开弹窗</button> <UserDialog ref="dialogRef" /> </template>

如果想让暴露接口更清晰,也可以单独声明类型:

exportinterfaceUserDialogExpose{open:()=>voidclose:()=>void}
<script setup lang="ts"> import type { UserDialogExpose } from './types' const exposed: UserDialogExpose = { open: () => {}, close: () => {} } defineExpose(exposed) </script>

七、Slot 类型简单了解

组件类型除了 Props、Emits、Ref,还有一个常见点是 Slot。Vue 3.3+ 可用defineSlots描述插槽参数:

<script setup lang="ts"> interface Row { id: number name: string } defineProps<{ list: Row[] }>() defineSlots<{ default(props: { row: Row; index: number }): any empty(): any }>() </script> <template> <div v-if="list.length"> <slot v-for="(row, index) in list" :key="row.id" :row="row" :index="index" /> </div> <slot v-else name="empty" /> </template>

父组件使用时,row与index会有类型提示:

<template> <UserList :list="users"> <template #default="{ row, index }"> {{ index }} - {{ row.name }} </template> <template #empty> 暂无数据 </template> </UserList> </template>

Slot 类型不是本文重点,但在封装表格、列表、弹窗 footer 时很常见,建议知道defineSlots这个入口。


八、面试聚焦

8.1 defineProps 泛型写法为何推荐?

泛型写法更贴合 TypeScript,复杂类型表达更自然,可复用外部类型,也能获得更完整的 IDE 推导。运行时对象写法适合需要 Vue 运行时校验的场景,二者按需求选择。

8.2 Props 解构会不会丢失响应性?

直接普通解构容易让后续代码失去对 props 更新的感知。需要解构并保持响应性时,使用toRefs(props);简单场景优先直接使用props.xxx。

8.3 defineEmits 如何限制参数?

可以用函数重载写法,也可以用 Vue 3.3+ 的命名元组写法:

constemit=defineEmits<{submit:[data:FormData]close:[]}>()

这样事件名、参数数量、参数类型都能被 TS 检查。

8.4 模板 ref 为什么要写 null?

组件挂载前 DOM 或子组件实例还不存在,所以初始值应为null:

constinputRef=ref<HTMLInputElement|null>(null)

使用时通过?.或在onMounted后访问。


九、易混淆点

  1. 运行时 props 写法有 Vue 校验;泛型写法更适合 TS 复杂类型。
  2. withDefaults用来给泛型 Props 设置默认值,数组和对象默认值建议写函数。
  3. props普通解构要谨慎,需要响应性时用toRefs。
  4. defineEmits不只是声明事件名,还可以约束参数。
  5. v-model本质是modelValue+update:modelValue。
  6. DOM ref 通常写成ref<HTMLInputElement | null>(null)。
  7. 组件 ref 通常写成ref<InstanceType<typeof Comp> | null>(null)。
  8. 子组件方法需要defineExpose后,父组件 ref 才能访问。

十、思考与练习

1.defineProps的运行时对象写法和泛型写法有什么区别?

解析:运行时对象写法有 Vue 的运行时校验,适合简单类型和默认值;泛型写法更适合复杂对象、联合类型、外部类型复用,TS 推导更自然。

2.泛型 Props 如何设置默认值?

解析:使用withDefaults(defineProps<Props>(), defaults),数组和对象默认值建议用函数返回。

3.为什么不建议随手写const { title } = defineProps<Props>()?

解析:普通解构可能带来响应性误用。需要保持响应性时用toRefs(props),否则优先props.title。

4.如何声明一个submit事件,参数为{ name: string; age: number }?

解析:

constemit=defineEmits<{submit:[form:{name:string;age:number}]}>()

5.父组件如何拿到子组件暴露的open方法?

解析:子组件defineExpose({ open }),父组件用ref<InstanceType<typeof Child> | null>(null)获取组件实例,并通过childRef.value?.open()调用。


总结

  • Props:推荐泛型写法,复杂类型更清晰;默认值用withDefaults。
  • Props 解构:优先props.xxx;需要响应性解构时用toRefs。
  • Emits:用类型约束事件名和参数,避免事件写散。
  • v-model:本质是 prop + update 事件,类型要同时约束。
  • 模板 ref:DOM ref 写元素类型,组件 ref 写InstanceType<typeof Comp>。
  • defineExpose:子组件显式暴露方法,父组件才能通过 ref 调用。

相关新闻

  • CPPM注册职业采购经理怎么报名?报考条件、费用和证书查询一次说清
  • 支付系统重复收费难题:幂等键依赖的四个假设及应对之策
  • 3分钟掌握BurpCrypto插件:实战DES加密登录接口自动化测试

最新新闻

  • 衡弈GTO_新手如何系统学德州GTO
  • 基于JavaScript的网盘直链解析工具:多平台API集成架构与高性能下载实现
  • 汽车维修厂业绩稳步增长实战总结(十):配件业务管理的价值与提升清单
  • 机器学习工程师能力进阶指南:你的“段位”到哪一级了?
  • 大数据仪表盘技术架构与性能优化实战
  • GPU算力解决方案全图谱——三大路线、代表平台与混合部署策略

日新闻

  • JMeter接口测试实战:从核心元件到复杂场景构建
  • Java Applet版刽子手游戏源码:含完整项目结构、吊杆绘图与胜负逻辑
  • 使用Apache JMeter对RoadRunner PHP应用进行性能测试与调优指南

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号