07-组件与 Props / Emits

发布时间:2026/7/1 13:06:22
07-组件与 Props / Emits 组件与 Props / Emits深入掌握 Vue3 组件定义方式、Props 类型验证、Emits 事件声明以及透传 Attributes 和多 v-model 的高级用法。一、前言组件是 Vue 应用的核心构建单元。Vue3 引入了script setup语法糖极大地简化了组件的定义方式。同时defineProps和defineEmits编译器宏让类型声明更加直观。本文将系统讲解 Vue3 中组件的完整定义方式、Props 和 Emits 的使用、透传 Attributes 的处理以及 Vue2 与 Vue3 在组件定义上的差异。二、在 Vue3 中定义组件2.1 SFC script setup 语法糖Vue3 推荐的使用方式是单文件组件SFC配合script setupscript setup import { ref } from vue // 直接声明响应式数据和方法 const count ref(0) const increment () { count.value } /script template div classcounter span{{ count }}/span button clickincrement1/button /div /template style scoped .counter { display: flex; gap: 12px; align-items: center; } /style2.2 组件的三种定义方式对比Vue3 组件定义方式Options APIComposition APIsetup 函数script setup语法糖data / methods /computed / watchsetup 返回模板所需数据顶层变量自动暴露给模板方式代码量TypeScript 支持推荐度Options API较多一般迁移旧项目时使用Composition API (setup)中等良好需要灵活控制时script setup最少优秀强烈推荐三、defineProps 宏3.1 基本用法defineProps是编译器宏不需要导入即可使用script setup // 运行时声明 const props defineProps({ title: { type: String, required: true }, count: { type: Number, default: 0 }, items: { type: Array, default: () [] // 对象/数组默认值使用工厂函数 }, config: { type: Object, default: () ({}), validator(value) { return value.theme light || value.theme dark } } }) console.log(props.title) // 访问 props /script3.2 基于类型的声明推荐使用 TypeScript 类型声明获得完整的类型推断script setup langts // 类型声明方式需配合 TypeScript interface Props { title: string count?: number items?: string[] config?: { theme: light | dark fontSize?: number } } // 带默认值的类型声明 const props withDefaults(definePropsProps(), { count: 0, items: () [], config: () ({ theme: light, fontSize: 14 }) }) /script3.3 Props 验证详解script setup const props defineProps({ // 基础类型检查 propA: Number, // 多个可能的类型 propB: [String, Number], // 必填字符串 propC: { type: String, required: true }, // 带默认值的数字 propD: { type: Number, default: 100 }, // 带默认值的对象 propE: { type: Object, default() { return { message: hello } } }, // 自定义验证函数 propF: { type: String, validator(value) { return [success, warning, danger].includes(value) } }, // 自定义类型构造函数 propG: { type: [String, null], // 允许 null required: true } }) /script3.4 Props 是只读的script setup const props defineProps({ initialCount: Number }) import { ref } from vue // 错误直接修改 props // props.initialCount // 警告 // 正确使用本地副本或计算属性 const count ref(props.initialCount) const increment () { count.value } /script四、defineEmits 宏4.1 基本用法script setup // 运行时声明 const emit defineEmits([update, delete, submit]) const handleUpdate () { emit(update, { id: 1, name: 新名称 }) } const handleDelete (id) { emit(delete, id) } const handleSubmit (formData) { // 事件验证返回 false 可阻止事件 emit(submit, formData) } /script4.2 基于类型的声明script setup langts // 类型声明方式 const emit defineEmits{ update: [id: number, name: string] delete: [id: number] submit: [formData: FormData] // 无参数事件 reset: [] }() const handleUpdate () { emit(update, 1, 新名称) } /script4.3 事件参数传递!-- ChildComponent.vue -- script setup const emit defineEmits([change, input, confirm]) const onChange (event) { // 传递单个参数 emit(change, event.target.value) } const onInput (value, oldValue) { // 传递多个参数 emit(input, value, oldValue) } const onConfirm () { // 传递对象参数 emit(confirm, { timestamp: Date.now(), confirmed: true }) } /script template div input changeonChange / button clickonConfirm确认/button /div /template!-- ParentComponent.vue -- script setup import ChildComponent from ./ChildComponent.vue const handleChange (value) { console.log(变更值:, value) } const handleConfirm (payload) { console.log(确认时间:, payload.timestamp) } /script template ChildComponent changehandleChange confirmhandleConfirm / /template五、defineExpose5.1 暴露组件方法默认情况下组件内部的属性和方法不会暴露给父组件。使用defineExpose显式暴露!-- ModalComponent.vue -- script setup import { ref } from vue const visible ref(false) const title ref() const open (modalTitle) { title.value modalTitle visible.value true } const close () { visible.value false } const confirm () { close() return true } // 显式暴露给父组件 defineExpose({ open, close, confirm, // 也可以暴露响应式数据 visible }) /script template div v-ifvisible classmodal div classmodal-header{{ title }}/div div classmodal-body slot / /div div classmodal-footer button clickconfirm确认/button button clickclose取消/button /div /div /template!-- ParentComponent.vue -- script setup import { ref } from vue import ModalComponent from ./ModalComponent.vue const modalRef ref(null) const showModal () { modalRef.value?.open(新增用户) } const handleConfirm () { const result modalRef.value?.confirm() if (result) { console.log(用户确认了操作) } } /script template div button clickshowModal打开弹窗/button ModalComponent refmodalRef p弹窗内容/p /ModalComponent /div /template六、透传 Attributes6.1 什么是透传 Attributes透传 Attributes 是指传递给组件但未在props中声明的属性常见的有class、style、id以及事件监听器!-- 父组件传递 class 和 click 事件 -- MyButton classbtn-primary idsubmit-btn clickhandleClick 提交 /MyButton!-- MyButton.vue -- template !-- class、id、click 会自动透传到 button 元素 -- button classbase-btn slot / /button /template最终渲染结果buttonclassbase-btn btn-primaryidsubmit-btn提交/button6.2 禁用 Attributes 继承有时需要完全控制透传行为script setup defineOptions({ inheritAttrs: false }) /script template div classwrapper !-- 使用 v-bind$attrs 手动绑定 -- button v-bind$attrsslot //button /div /template6.3 访问 $attrs在script setup中访问透传属性script setup import { useAttrs } from vue const attrs useAttrs() // attrs 包含所有未声明为 props 的属性和事件 console.log(attrs.class) console.log(attrs.onClick) // 注意attrs 不是响应式的不要解构 // 如需响应式访问使用 computed import { computed } from vue const customClass computed(() attrs.class) /script6.4 多根节点组件的透传Vue3 支持多根节点组件但透传行为有所不同script setup defineOptions({ inheritAttrs: false }) const attrs useAttrs() /script template !-- 多根节点必须显式指定 $attrs 的绑定位置 -- header v-bind$attrs标题/header main内容/main footer底部/footer /template七、多个 v-model7.1 基础 v-model!-- CustomInput.vue -- script setup const props defineProps({ modelValue: String }) const emit defineEmits([update:modelValue]) const onInput (event) { emit(update:modelValue, event.target.value) } /script template input :valuemodelValue inputonInput placeholder请输入 / /template!-- 父组件使用 -- script setup import { ref } from vue import CustomInput from ./CustomInput.vue const message ref() /script template CustomInput v-modelmessage / p输入内容: {{ message }}/p /template7.2 多个 v-modelVue3 支持在同一个组件上使用多个v-model!-- UserForm.vue -- script setup const props defineProps({ firstName: String, lastName: String, age: Number }) const emit defineEmits([ update:firstName, update:lastName, update:age ]) const updateField (field, value) { emit(update:${field}, value) } /script template div classuser-form input :valuefirstName inputupdateField(firstName, $event.target.value) placeholder姓 / input :valuelastName inputupdateField(lastName, $event.target.value) placeholder名 / input typenumber :valueage inputupdateField(age, Number($event.target.value)) placeholder年龄 / /div /template!-- 父组件使用 -- script setup import { reactive } from vue import UserForm from ./UserForm.vue const user reactive({ firstName: 张, lastName: 三, age: 25 }) /script template UserForm v-model:first-nameuser.firstName v-model:last-nameuser.lastName v-model:ageuser.age / pre{{ user }}/pre /template7.3 v-model 修饰符Vue3 支持自定义v-model修饰符!-- MyInput.vue -- script setup const props defineProps({ modelValue: String, modelModifiers: { type: Object, default: () ({}) } }) const emit defineEmits([update:modelValue]) const onInput (event) { let value event.target.value // 应用修饰符 if (props.modelModifiers.trim) { value value.trim() } if (props.modelModifiers.upper) { value value.toUpperCase() } if (props.modelModifiers.number) { value Number(value) || 0 } emit(update:modelValue, value) } /script template input :valuemodelValue inputonInput / /template!-- 使用自定义修饰符 -- MyInput v-model.trim.uppertext / MyInput v-model.numbernum /八、Vue2 vs Vue3 组件定义对比8.1 完整对比表特性Vue2Vue3组件定义Options APIOptions API / Composition API /script setupProps 声明props选项defineProps宏Emits 声明直接使用this.$emitdefineEmits宏推荐显式声明暴露方法自动暴露defineExpose显式暴露多 v-model不支持支持v-model 参数valueinputmodelValueupdate:modelValue透传属性$attrs包含class/style$attrs不包含class/style多根节点不支持支持组件名推断依赖文件名支持defineOptions8.2 代码对比同一组件的三种写法Vue2 Options APItemplate div input :valuevalue inputonInput / button clickonConfirm确认/button /div /template script export default { props: { value: String }, methods: { onInput(e) { this.$emit(input, e.target.value) }, onConfirm() { this.$emit(confirm, this.value) } } } /scriptVue3 Composition APItemplate div input :valuemodelValue inputonInput / button clickonConfirm确认/button /div /template script import { defineComponent } from vue export default defineComponent({ props: { modelValue: String }, emits: [update:modelValue, confirm], setup(props, { emit }) { const onInput (e) { emit(update:modelValue, e.target.value) } const onConfirm () { emit(confirm, props.modelValue) } return { onInput, onConfirm } } }) /scriptVue3script setup推荐script setup const props defineProps({ modelValue: String }) const emit defineEmits([update:modelValue, confirm]) const onInput (e) { emit(update:modelValue, e.target.value) } const onConfirm () { emit(confirm, props.modelValue) } /script template div input :valuemodelValue inputonInput / button clickonConfirm确认/button /div /template九、组件命名与自动导入9.1 使用 defineOptions 设置组件名script setup defineOptions({ name: MyCustomComponent }) /script9.2 自动导入配置使用unplugin-vue-components实现组件自动导入// vite.config.jsimport{defineConfig}fromviteimportvuefromvitejs/plugin-vueimportComponentsfromunplugin-vue-components/viteexportdefaultdefineConfig({plugins:[vue(),Components({// 自动导入 src/components 下的组件dirs:[src/components],// 自动导入 Element Plus 组件resolvers:[// ElementPlusResolver()]})]})十、常见问题Q1: defineProps 和 defineEmits 需要导入吗在script setup中不需要导入它们是编译器宏。但如果使用 TypeScript 类型声明方式需要确保vue版本支持。Q2: 如何在 script setup 中访问 props 的值script setup const props defineProps({ title: String }) // 直接访问 console.log(props.title) // 在模板中直接使用不需要 props. /script template h1{{ title }}/h1 /templateQ3: 父组件如何监听子组件的所有事件script setup const handleAllEvents (eventName, ...args) { console.log(事件:, eventName, 参数:, args) } /script template !-- 使用 v-on 监听所有事件 -- ChildComponent v-onhandleAllEvents / /templateQ4: 多个 v-model 的命名规范是什么使用camelCase声明使用时转换为kebab-casescript setup // 声明时使用 camelCase defineProps({ firstName: String, lastName: String }) /script !-- 使用时用 kebab-case -- UserForm v-model:first-namefirst v-model:last-namelast /十一、总结Vue3 的组件系统通过script setup和编译器宏大幅提升了开发体验defineProps声明组件接收的属性支持运行时验证和 TypeScript 类型推断defineEmits声明组件触发的事件实现类型安全的事件通信defineExpose显式控制组件暴露的接口增强封装性透传 Attributes$attrs和inheritAttrs灵活控制属性透传行为多 v-model支持同一组件绑定多个双向数据流代码简洁度script setup相比 Options API 减少了大量样板代码十二、思考题/练习代码练习封装一个BaseInput组件支持v-model、placeholder、disabled等属性以及focus、blur事件透传。类型挑战使用 TypeScript 为以下组件编写完整的 Props 和 Emits 类型声明interfaceProps{items:Array{id:number;name:string}selectedId?:numberloading?:boolean}interfaceEmits{select:[id:number]remove:[id:number]refresh:[]}组件封装实现一个ConfirmDialog组件支持通过ref调用open()和close()方法使用defineExpose暴露接口。多 v-model 实践创建一个RangeSlider组件同时支持v-model:min和v-model:max两个双向绑定。