Vue3的多根节点组件与父子组件之间的事件继承

多根节点组件

        Vue.js 3.x 版本不再对组件的 template 选项进行“唯一根节点”的限制,而是允许 DOM 结构具备多个根节点,即下列结构的全局组件在 Vue.js 3.x 版本中是允许的。
app.component('my-box',{
        template:`
                <div class="first">第一个节点</div>
                <div class="second">第二个节点</div>
                <div class="third">第三个节点</button>
        `
})
本节对这种结构可能出现的问题以及 Vue 框架的语法要求进行讲解。

1、 Prop 属性

        在使用组件时,被子组件通过 props 选项接收的属性被称为“ Prop 属性”,没有被子组件的 props 选项接收的属性被称为“非 Prop 属性”。 Vue 框架规定: Prop 属性可以向子组件数据区那样进行调用,即使用 this 就可以访问到 Prop 属性;非 Prop 属性需要在子组件中使用 this.$attrs 来进行访问。
        【示例 4-15 Prop 属性的访问 。注册一个全局组件名为 my-phone ,其中设置一个 Prop 属性名为brand,再设置一个非 Prop 属性名为 count 。最终在组件的 template 选项中设置两个 div 容器访问这两个属性。
HTML 文档中对 my-phone 组件使用的代码如下所示。
<div id="app">
    <my-phone brand="HuaWei" :count="150"></my-phone>
</div>
Vue 代码如下所示。
01 let app=Vue.createApp({})
02     app.component('my-phone',{
03         template:`
04             <div class="brand">品牌:{{this.brand}}</div>
05             <div class="count">数量:{{this.$attrs.count}}</div>
06         `,
07     props:['brand']
08 })
09 app.mount('#app')
        上述代码中,<my-phone> HTML 代码中设置了两个参数: brand count 。在组件注册的代码中,第07 行将 brand 参数利用 props 选项进行接收,形成了 Prop 属性。而 count 参数没有用 props 选项进行接收,即是一个非 Prop 属性。
        在组件的 template 模板中,第 04 行访问了 Prop 属性 brand ,使用的是 this.brand 格式。第 05 行访问了非 Prop 属性 count ,使用的是 this.$attrs.count 格式。
        这里需要注意,非 Prop 属性也是单向数据流传递,即不能在组件注册代码中改变该属性的值,也不能将该变量通过 v-model 绑定在表单元素中。
        同理,在使用组件时绑定的自定义事件,也可以通过 this$attrs 获取。与非 Prop 属性不同的是,绑定事件的名称,在 this.$attrs 中需要添加 on_ 前缀。
        【示例 4-16 通过 this.$attrs 触发组件的事件 。注册一个全局组件名为 my-alert ,为该组件设置一个名为 message 的属性,在绑定一个名为 custom 的自定义事件。
HTML 文档中对 my-alert 组件使用的代码如下所示。
<div id="app">
    <my-alert message="非 Prop 属性的使用" @custom="custom"></my-alert>
</div>
Vue 代码如下所示。
01 let app=Vue.createApp({
02 methods:{
03 custom(value){
04 window.alert(value)
05 }
06 }
07 })
08 app.component('my-alert',{
09 template:`
10 <button @click="btnClick">单击</button>
11 `,
12 methods:{
13 btnClick(){
14 this.$attrs.onCustom(this.$attrs.message);
15 }
16 }
17 })
18 app.mount('#app')
        在上述代码中,HTML 文档使用 <my-alert> 组件时,设置了一个自定义属性 message ,同时绑定了一个自定义事件 custom 。由于在组件定义的 Vue 代码中,没有使用 props 来接收自定义属性 message ,因此message 是一个非 Prop 属性。对于绑定的自定义事件,可以在组件中使用 this.$emit() 进行调用,也可以使用 this.$attrs 进行访问。
        从第 14 行中可以看出,自定义事件 custom this.$attrs 对象中存储为 onCustom ,这是读者需要注意的地方,触发该事件时,需要使用 this.$attrs.onCustom() 来实现。

2、 组件事件的继承

        Vue.js 3.0 框架规定:父组件的事件可以直接继承给具备该事件的子组件及其后代组件。

 

        【示例 4-17 组件事件的继承 。书写一个用于选择专业的组件 select-specialty 。该组件中具备一个可供选择专业的下拉菜单。当用户选择某个专业的菜单项时,需要用弹窗在页面中显示选中的专业内容。

 

        从本示例的要求来看,应该为组件中的下拉菜单绑定 input change 事件,为了体现出组件事件的可传递性,我们将预计绑定到下拉菜单的 input 事件绑定在该组件的父组件中。测试该功能是否能够实现。
HTML 文档中对 select-specialty 组件使用的代码如下所示。
<div id="app">
    <select-specialty @input="boxInput"></select-specialty>
</div>
Vue 代码如下所示。
01 let app=Vue.createApp({
02 methods:{
03 boxInput(){
04 window.alert(event.target.value);
05 }
06 }
07 })
08 app.component('select-specialty',{
09 template:`
10 <select v-model="specialty">
11 <option>前端工程师</option>
12 <option>PHP 工程师</option>
13 <option>Java 工程师</option>
14 <option>UI 设计师</option>
15 <option>项目架构师</option>
16 </select>
17 `,
18 data(){
19 return {
20 specialty:'UI 设计师'
21 }
22 }
23 })
24 app.mount('#app')
        在上述代码中,input 事件绑定在了 HTML 文档中使用 <select-specialty> 组件的代码中,即该组件的父组件中。由于父组件中的事件子组件成员(即<select> 标记对)也有,因此该事件会传递给子组件成员。

 

        若不希望父组件的事件传递给子组件,可以在子组件的定义代码中使用 inheritAttrs 选项,该选项取值为 false 时,this.$attrs 对象依然可以收集父组件传递过来的属性和绑定的事件,但是父组件绑定的事件不会在继承给子组件成员,即子组件成员的相应事件触发后,也不会执行任何父组件事件的代码。

若要让组件 select-specialty 进制父子组件之间的事件传递,代码如下所示。

01 app.component('select-specialty',{
02     inheritAttrs: false,
03     template:`
04         <select v-model="specialty">
05             <option>前端工程师</option>
06             <option>PHP 工程师</option>
07             <option>Java 工程师</option>
08             <option>UI 设计师</option>
09             <option>项目架构师</option>
10         </select>
11     `,
12     data(){
13         return {
14             specialty:'UI 设计师'
15         }
16     }
17 })
        在上述代码中,第 02 行为组件设置了 inheritAttrs 选项,并取值为 false ,这样父组件绑定的 input 事件在第 04 行的 <select> 子组件成员中就不会起作用了。

3、 多根节点组件的规定

        从 Vue.js 3.x 框架开始,组件的 template 选项可以接收多个根节点结构。在 Vue.js 2.x 框架中,对 template选项有必须是一个根节点的限制。
        若组件具备多个根节点,同时父组件绑定了事件,根据默认情况下父组件的事件会继承给子组件的这一特性,Vue 框架会报出一个警告。警告内容如下所示。
       
        [Vue warn]: Extraneous non-emits event listeners (click) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the "emits" option.
        上述警告的含义是:外部的(即父组件)的非触发的事件监听器(单击事件)被传递给子组件,但是不能自动继承,因为组件将渲染为片段或文本根节点。如果侦听器只是组件自定义事件的侦听器,则需要使用“emit ”选项来声明它。
        上述警告的解决方案有以下两个:
         禁止父组件向子组件继承事件:即在子组件中书写 inheritAttrs:false 选项。
         在子组件中需要继承父组件事件的根节点上显式绑定 $attrs ,这被称为“隐式贯穿”行为。

 

        【示例 4-18 多根节点组件的事件继承 。书写一个名为 root-test 的组件,其中包含三个根节点。在父组 件中绑定单击事件,显式指定第二个根节点继承父组件的事件。
        HTML 文档中对 select-specialty 组件使用的代码如下所示。
<div id="app">
    <root-test @click="divClick"></root-test>
</div>
Vue 代码如下所示。
01 let app=Vue.createApp({
02     methods:{
03         divClick(){
04             window.alert(event.target.textContent);
05         }
06     }
07 })
08 app.component('root-test',{
09 // inheritAttrs: false,
10     template:`
11         <div class="first">第一个根节点</div>
12         <div class="second" v-bind="$attrs">第二个根节点</div>
13         <div class="third">第三个根节点</div>
14     `
15 })
16 app.mount('#app')
        Vue 框架的官网指出:与单个根节点组件不同,具有多个根节点的组件不具有自动隐式贯穿行为( Auto fallthrough)。若未显式绑定 $attrs ,则会在运行时发出警告。
        在上述代码中,第 10 行至第 14 行定义了组件中存在的三个根节点,同时父组件上绑定了 click 事件。 要想放置警告的发生,可以将第 09 行注释变为语句,禁止父组件将事件传递给子组件成员,这也是我们讲解的第一种警告解决方案。也可以像第 12 行那样,使用 v-bind 绑定 $attrs ,这样就指定了只有第二个根节点可以继承父组件的事件,同时也不会在运行时发出警告了。
<think>好的,我需要帮助用户理解如何在Vue 3中同时使用defineProps和defineEmits,特别是如何避免预期的事件监听器问题。根据用户之前的对话,他们已经遇到了关于事件监听的警告,所以需要具体说明如何正确声明props和emits来避免这种情况。 首先,用户提到参考了站内的引用,其中引用了React和TypeScript的最佳实践,以及Vue组件定义的注意事项,还有依赖注入库。不过这些引用可能当前问题关联不大,但需要确认是否有关联点。用户的主要问题集中在Vue 3中如何结合使用defineProps和defineEmits,特别是最佳实践和示例。 用户的上一个问题涉及Vue警告emit事件监听器的问题,解决方案中提到了使用emits选项声明自定义事件。现在用户希望更深入了解如何同时使用defineProps和defineEmits,可能是在组合式API(Composition API)中的用法,因为defineProps和defineEmits通常在该API中使用。 需要明确以下几点: 1. 在Vue 3的组合式API中,defineProps和defineEmits是编译器宏,用于在<script setup>中声明props和emits。 2. 使用defineEmits正确声明事件可以避免之前的警告,即父组件传递了未被声明的事件监听器。 3. 需要展示如何在同一个组件中同时定义props和emits,并提供示例代码。 4. 最佳实践包括显式声明emits,类型声明(使用TypeScript),以及如何处理可选props和默认值。 5. 可能还需要说明在模板中如何触发事件,以及父组件如何监听这些事件。 需要确保回答结构清晰,分步骤说明,并包含示例代码。同时,用户可能需要了解TypeScript中的类型声明,因为引用中提到了TypeScript最佳实践,但用户的问题并未明确要求,但可以作为一个加分点提及。 需要验证以下几点是否正确: - 在<script setup>中,defineProps和defineEmits不需要导入,直接使用。 - 使用defineEmits声明事件后,Vue不会发出警告,因为事件已被显式声明。 - 当组件多根节点时,需注意事件监听器的绑定位置,或者是否需要使用attrs。 可能的示例应包括: - 基本用法:同时定义props和emits。 - 类型声明(使用TypeScript)。 - 触发事件并传递数据。 - 父组件监听事件的正确方式。 还需要考虑常见错误,比如拼写错误、未声明的事件、或者忘记在模板中触发事件。同时,结合用户之前的警告,强调声明emits的重要性,以及如何通过defineEmits来解决该问题。 总结回答的结构: 1. 基本用法:如何在<script setup>中使用defineProps和defineEmits。 2. 类型安全:结合TypeScript的类型声明。 3. 事件触发:在方法中调用emit函数。 4. 父组件监听:如何在父组件中使用子组件事件。 5. 最佳实践:显式声明、类型定义、单一职责等。 6. 常见问题:未声明事件、拼写错误、多根组件的问题。 需要确保示例代码正确,特别是emit的调用和事件监听的语法。同时,可能需要指出在组合式API中,不需要在emits选项中再次声明,因为defineEmits已经处理了这一点。 最后,生成的相关问题需要Vue 3props和emits使用相关,可能涉及TypeScript、父子组件通信、最佳实践等。</think>### 解答:Vue 3 中 `defineProps` 和 `defineEmits` 联合使用的最佳实践 #### 一、核心作用语法 在 Vue 3 的组合式 API(`<script setup>`)中,`defineProps` 和 `defineEmits` 是**编译器宏**,用于声明组件props 和自定义事件。通过显式声明可解决预期事件监听器警告[^1]。 ```html <script setup> // 定义 props 类型和默认值 const props = defineProps({ title: { type: String, required: true }, count: { type: Number, default: 0 } }); // 定义 emits 事件及参数类型 const emit = defineEmits<{ (e: &#39;update:title&#39;, value: string): void; (e: &#39;increment&#39;, delta: number): void; }>(); </script> ``` --- #### 二、联合使用场景示例 ##### 1. 父子组件双向绑定 通过 `v-model` 和自定义事件实现双向数据流: ```html <!-- 子组件 Child.vue --> <script setup> const props = defineProps([&#39;modelValue&#39;]); const emit = defineEmits([&#39;update:modelValue&#39;]); const updateValue = (newVal) => { emit(&#39;update:modelValue&#39;, newVal); }; </script> <template> <input :value="modelValue" @input="updateValue($event.target.value)"> </template> ``` ```html <!-- 父组件 --> <Child v-model="data" /> ``` ##### 2. 类型安全(TypeScript) 为 props 和 emits 添加类型约束: ```typescript interface Props { items: string[]; selectedId?: number; } interface Emits { (e: &#39;select&#39;, id: number): void; (e: &#39;close&#39;): void; } const props = defineProps<Props>(); const emit = defineEmits<Emits>(); ``` --- #### 三、最佳实践 1. **显式声明事件** 始终通过 `defineEmits` 声明自定义事件,避免预期监听器警告[^1]。 ```javascript // 错误:未声明事件会触发警告 const emit = defineEmits(); // 缺少事件声明 // 正确 const emit = defineEmits([&#39;submit&#39;, &#39;cancel&#39;]); ``` 2. **类型优先的 Props 定义** 使用 TypeScript 接口提升可维护性: ```typescript defineProps<{ disabled?: boolean; size: &#39;small&#39; | &#39;medium&#39; | &#39;large&#39;; }>(); ``` 3. **单一职责原则** 若组件需要同时处理多个事件,拆分逻辑到独立函数: ```javascript const handleSubmit = () => { emit(&#39;submit&#39;, formData); }; const handleCancel = () => { emit(&#39;cancel&#39;); }; ``` 4. **默认值校验** 为可选 props 提供默认值: ```javascript const props = defineProps({ limit: { type: Number, default: 10, validator: (value) => value > 0 } }); ``` --- #### 四、常见问题解决 | 问题场景 | 解决方案 | |------------------------------|--------------------------------------------------------------------------| | 事件名拼写不一致 | 统一父子组件中的事件名称(如 `kebab-case` 格式:`@update-title`) | | 未传递必填 props | 通过 TypeScript 接口标记 `required: true` 或使用可选链操作符 `props?.id` | | 多根组件事件继承问题 | 在需要接收事件的根元素上手动绑定 `v-bind="$attrs"` | | 误用原生 DOM 事件名(如 `click`) | 自定义事件名避免原生事件冲突,或使用 `emits: [&#39;click&#39;]` 明确声明 | --- #### 五、完整示例 ```html <!-- 子组件 FormInput.vue --> <script setup> const props = defineProps({ label: { type: String, required: true }, value: { type: [String, Number], default: &#39;&#39; } }); const emit = defineEmits<{ (e: &#39;update:value&#39;, val: string): void; (e: &#39;focus&#39;): void; }>(); const handleInput = (e) => { emit(&#39;update:value&#39;, e.target.value); }; </script> <template> <div class="form-group"> <label>{{ label }}</label> <input :value="value" @input="handleInput" @focus="emit(&#39;focus&#39;)" > </div> </template> ``` ```html <!-- 父组件 --> <FormInput label="用户名" :value="username" @update:value="username = $event" @focus="logFocusEvent" /> ``` --- ### 相关问题 1. 如何在 Vue 3 中为 `defineProps` 定义复杂对象类型? 2. `defineEmits` 和 Options API 中的 `emits` 选项有何区别? 3. 如何处理 Vue 3 多根组件中的属性继承问题? 4. 为什么 TypeScript 有时无法推断 `defineProps` 的类型?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值