Vue3的inheritAttrs属性到底是干嘛的?用它能解决我哪些实际开发痛点?
Vue3里的inheritAttrs基础规则变了?先搞懂最核心的两点
你没看错,相比Vue2,Vue3的inheritAttrs默认行为没变,但适用范围和操作细节悄悄升级了,先别急着看场景,把核心规则掰碎嚼烂,后面遇到问题才能秒懂该怎么调。
第一点:什么是“非props/非emits的根属性”?
不管是Vue2还是Vue3,inheritAttrs的作用对象都只有一类——那些没在组件的props选项里声明、也没在emits选项里注册(Vue3新增的绑定逻辑相关)的HTML原生属性、自定义属性(包括事件监听器)。
举个Vue3新手常犯的例子吧,比如你写了一个通用的输入框组件MyInput.vue,props只写了modelValue(双向绑定的必传值,Vue3用v-model默认绑这个),然后在父组件里这么用:
<!-- 父组件Parent.vue -->
<template>
<div class="parent-container">
<MyInput
v-model="inputVal"
type="password"
placeholder="请输入密码"
data-testid="my-pwd-input"
@focus="handlePwdFocus"
@blur="handlePwdBlur"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
import MyInput from './MyInput.vue';
const inputVal = ref('');
const handlePwdFocus = () => console.log('密码框聚焦了');
const handlePwdBlur = () => console.log('密码框失焦了');
</script>
这时候你打开浏览器的元素检查器,会发现什么?哦对,如果MyInput.vue是最简单的单根组件写法:
<!-- MyInput.vue 单根写法 --> <template> <input v-model="modelValue" /> </template> <script setup> defineProps(['modelValue']); // 没声明type、placeholder、data-testid defineEmits(['update:modelValue']); // 没注册focus、blur </script>
那父组件传的type、placeholder、data-testid、@focus、@blur,会被自动挂载到MyInput.vue模板的根元素(也就是那个input标签)上——focus和blur还会自动作为原生事件监听,这时候,如果你在MyInput.vue的script里打印useAttrs()(Vue3新增的API,替换了Vue2的$attrs),会看到这些未声明/注册的属性和事件的完整对象,对吧?
但这里有个关键点:Vue3里只有没有被显式处理的非props/非emits才会触发inheritAttrs的挂载,什么叫“显式处理”?比如你用v-bind="$attrs"把这些属性绑到了模板里的某个非根元素上,或者在script里用了useAttrs()里的某个属性去做逻辑判断,或者在emits里注册了focus/blur但没在模板里处理(哦不对,显式emit触发不算,只有模板里用v-on绑定显式触发的才算处理?不对不对,等下仔细理——显式处理是指“组件内部主动接收并决定怎么用这些非根绑定的内容”:
- 事件方面:不管你是在模板里直接绑给某个元素用@focus="handleFocusInChild",还是在script里通过onMounted+document.addEventListener+useAttrs().onFocus,或者在emits里注册后直接转发(defineEmits(['focus']); 然后模板里<input @focus="$emit('focus')">),只要内部不是“完全不管事件,等着自动绑根元素”,就算显式处理了focus;
- 属性方面:同理,不管是v-bind到非根元素,还是在style绑定里用,还是在逻辑里用useAttrs().type去切换输入框样式,只要内部主动用了,就算处理,inheritAttrs的默认挂载就会排除这个属性。
哦还有,Vue3里的v-model默认拆成了modelValue prop和update:modelValue emit,这俩如果声明/注册了,就不算在inheritAttrs的作用对象里——这点和Vue2是一致的,但Vue3支持多个v-model绑定,这个后面场景里会提。
第二点:默认值没变,但多根组件的规则变了!
很多刚转Vue3的朋友踩的第一个坑,就是多根组件的问题,先回忆下Vue2的多根组件(那时候叫Fragment组件,但Vue2是不支持的,得加一个外层div或者span当根)——哦不对,Vue2根本不支持原生多根组件,必须有唯一的根元素,所以inheritAttrs只能挂载那个唯一的根。
但Vue3原生支持多根组件了!那这时候inheritAttrs还能自动挂载吗?不能! 因为Vue不知道你想把这些非根绑定的内容绑到哪一个根元素上,这时候,如果你写了一个多根组件,又没显式关闭inheritAttrs或者显式用v-bind="$attrs"指定挂载位置,Vue会在控制台给你一个友好的警告提示:“Non-prop attribute passed to component with multiple root nodes will be ignored.”对吧?
所以多根组件的第一条铁律就是:要么显式设置inheritAttrs: false,要么显式用v-bind="$attrs"把这些非props/非emits绑到你指定的根元素上——绝对不能模棱两可。
inheritAttrs和$attrs到底有什么区别?别再搞混了!
好多人把这俩当成一个东西,这完全是错误的,简单粗暴的一句话概括:inheritAttrs是开关,控制非props/非emits的自动挂载行为;$attrs(Vue3用useAttrs()函数获取,返回的对象就是$attrs)是容器,存储所有这些未声明/注册的内容,不管开关开还是关。
敲黑板,这个区别太重要了!哪怕你把inheritAttrs设为false,useAttrs()还是能拿到所有的父组件传过来的非根绑定内容——你只是不让Vue自动帮你绑到根元素上而已,主动权完全在你手里。
再举个具体的例子,你就能彻底分清了: 还是刚才的MyInput.vue,现在改成多根组件,而且要把父组件传的data-testid绑到外层的label上,type、placeholder、focus/blur绑到input上,inheritAttrs设为false:
<!-- MyInput.vue 多根+inheritAttrs:false写法 -->
<template>
<label data-testid="my-pwd-input-label"> <!-- 显式用文字写死也可以,但这里假设父组件要传label的testid?哦不对,刚才父组件传的是data-testid="my-pwd-input",应该把data-testid从$attrs里拆出来绑label,剩下的绑input -->
<label v-bind="{ 'data-testid': attrs['data-testid'] }">
请输入密码:
<input
v-model="modelValue"
v-bind="restAttrs"
/>
</label>
<span v-if="showError" class="error-msg">{{ errorMsg }}</span>
</template>
<script setup>
import { computed } from 'vue';
// 显式关闭自动挂载
defineOptions({
inheritAttrs: false
});
// 显式声明props
const props = defineProps(['modelValue', 'showError', 'errorMsg']);
// 获取所有非props/非emits的内容
const attrs = useAttrs();
// 拆分成需要绑label和input的两部分
const restAttrs = computed(() => {
const { 'data-testid': _, ...rest } = attrs;
return rest;
});
</script>
这时候你再打开元素检查器:
- data-testid="my-pwd-input"绑到了外层的label上(显式拆出来的);
- type="password"、placeholder="请输入密码"、focus/blur事件绑到了input上(v-bind restAttrs);
- 控制台没有警告,因为我们显式指定了所有非根绑定的去处;
- inheritAttrs设为false了,但useAttrs()还是拿到了所有内容,对吧?
如果这时候你把inheritAttrs改成true,控制台就会跳出那个多根的警告,因为Vue找不到唯一的根元素去自动挂载。
什么时候该开inheritAttrs?什么时候该关?这几个场景最实用
讲完了基础概念和区别,咱们来聊最接地气的部分——实际开发中,到底什么时候碰这个开关?这里总结了我用Vue3两年多遇到的5个高频必碰场景,从简单到复杂,都是真真切切踩过坑或者提升过效率的。
单根通用组件,直接绑定到根元素最省事
这个场景其实就是inheritAttrs默认存在的意义——当你的组件是单根、且根元素就是对外暴露的核心交互/展示元素的时候,直接开着默认值(true),啥都不用管,父组件传的原生属性、自定义data-*属性、原生事件(除了被显式处理的)都会自动绑上去,代码特别干净,复用性也高。
刚才的单根MyInput.vue就是最好的例子,再扩展一个通用的按钮组件吧:
<!-- Button.vue 单根通用组件,默认inheritAttrs:true -->
<template>
<button
class="base-btn"
:class="[`btn-${size}`, `btn-${type}`]"
:disabled="loading || disabled"
>
<span v-if="loading" class="loading-spinner"></span>
<slot></slot>
</button>
</template>
<script setup>
defineProps({
size: {
type: String,
default: 'medium' // 可选small/medium/large
},
type: {
type: String,
default: 'primary' // 可选primary/secondary/warning/danger
},
loading: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
});
</script>
<style scoped>
.base-btn {
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
/* 基础样式 */
}
.btn-primary { background-color: #409eff; color: white; }
.btn-secondary { background-color: #f5f7fa; color: #606266; border: 1px solid #dcdfe6; }
/* 其他size、type、loading-spinner的样式省略 */
</style>
你看,这个按钮组件的根元素就是核心的button标签,父组件传的type="reset"(HTML原生的reset按钮类型,不是我们组件的type哦!这里要注意命名冲突,后面讲常见误区会提)、id="submit-btn"、data-form="login-form"、@click="handleSubmit"、@mouseenter="handleBtnHover",都会自动绑到这个button上,完全不用我们在Button.vue里显式写v-bind="$attrs"或者v-on监听这些事件——是不是特别爽?
不过这里有个小技巧:如果父组件传的原生属性和组件的prop重名了,Vue会优先用prop的逻辑,比如刚才的Button.vue里有个prop叫type,如果父组件传了type="reset",那会先触发组件的type="reset"样式(如果有的话),而不是把HTML原生的type="reset"绑到button上——这时候如果要传HTML原生的属性和组件prop重名怎么办?后面讲常见误区的时候会专门讲这个问题。
多根组件,必须显式指定绑定位置
这个刚才已经举过例子了,再补一个更复杂的、带嵌套插槽的多根组件场景,比如通用的表单输入项组件,包含label、input/select、错误提示三个根元素:
<!-- FormItem.vue 多根通用表单输入项 -->
<template>
<label
class="form-item-label"
:for="`form-item-${name}`"
v-bind="{ 'data-label-id': attrs['data-label-id'] }"
>
{{ label }}
<span v-if="required" class="required-asterisk">*</span>
</label>
<!-- 嵌套插槽,让父组件可以传input/select/textarea等任意输入控件 -->
<div class="form-item-input-wrapper">
<slot v-bind="restAttrs"></slot>
</div>
<span v-if="showError" class="form-item-error">{{ errorMsg }}</span>
</template>
<script setup>
import { computed } from 'vue';
defineOptions({
inheritAttrs: false
});
const props = defineProps({
label: { type: String, required: true },
name: { type: String, required: true }, // 用于绑定label的for和input的id
required: { type: Boolean, default: false },
showError: { type: Boolean, default: false },
errorMsg: { type: String, default: '' }
});
const attrs = useAttrs();
// 这里的restAttrs会通过作用域插槽传给父组件的输入控件
const restAttrs = computed(() => {
const { 'data-label-id': _, ...rest } = attrs;
// 顺便把组件的name绑到输入控件的id上,避免父组件重复传
rest.id = `form-item-${props.name}`;
return rest;
});
</script>
<style scoped>
/* 样式省略,form-item-label左对齐,input-wrapper占满剩余空间,error-msg红色 */
</style>
父组件这么用:
<!-- 父组件LoginForm.vue -->
<template>
<form class="login-form">
<FormItem
label="用户名"
name="username"
required
show-error="usernameError"
error-msg="请输入6-20位的字母数字组合"
data-label-id="login-username-label"
>
<!-- 这里用作用域插槽的attrs,也就是FormItem里的restAttrs,自动把id、type、placeholder、data-testid等绑上去 -->
<template #default="slotAttrs">
<input
v-model="formData.username"
v-bind="slotAttrs"
type="text"
placeholder="请输入用户名"
data-testid="login-username-input"
/>
</template>
</FormItem>
<!-- 密码项同理省略 -->
<Button type="primary" loading="loginLoading" @click="handleLogin">登录</Button>
</form>
</template>
这个场景里,我们不仅显式关闭了inheritAttrs避免多根警告,还把非根绑定的内容拆成了两部分(data-label-id绑外层label,剩下的加了id通过作用域插槽传给父组件的输入控件),大大提升了这个FormItem组件的复用性——不管父组件传的是input、select、textarea还是第三方的日期选择器、上传组件,都能完美适配!
组件内部有多个子元素,父组件的非根绑定要绑到非根上
这个场景和多根组件有点像,但它是单根组件——比如单根的搜索框组件,根元素是一个div,里面包含了一个input、一个放大镜icon、一个清除icon,这时候父组件传的type、placeholder、focus/blur等肯定要绑到input上,而不是外层的div上,对吧?
这时候inheritAttrs必须设为false,然后显式用v-bind="$attrs"把这些内容绑到input上:
<!-- SearchInput.vue 单根+非根绑定 -->
<template>
<div class="search-input-wrapper">
<span class="search-icon">🔍</span>
<input
v-model="innerVal"
v-bind="$attrs"
@input="handleInput"
/>
<span v-if="innerVal" class="clear-icon" @click="handleClear">✕</span>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
defineOptions({
inheritAttrs: false
});
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const innerVal = ref(props.modelValue);
// 双向绑定同步
watch(() => props.modelValue, (newVal) => {
innerVal.value = newVal;
});
watch(innerVal, (newVal) => {
emit('update:modelValue', newVal);
});
const handleClear = () => {
innerVal.value = '';
};
</script>
<style scoped>
.search-input-wrapper {
display: flex;
align-items: center;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
}
.search-icon, .clear-icon {
color: #909399;
cursor: pointer;
margin: 0 8px;
}
.search-input-wrapper input {
flex: 1;
border: none;
outline: none;
padding: 8px 0;
}
</style>
这时候父组件传的type="search"、placeholder="搜索文章"、maxlength="50"、@focus="showSearchHistory"、@blur="hideSearchHistory",都会自动绑到内部的input上,外层的div只会有我们自己写的class="search-input-wrapper"——完美!
这个场景应该是Vue3新手和老手都会遇到的高频场景,比如通用的下拉选择器、通用的滑块、通用的开关组件,大多数都是这种“根元素是包装div,核心交互/展示在内部子元素上”的结构,这时候一定要记得关inheritAttrs,然后把$attrs绑到内部的核心元素上。
自定义组件需要转发非props/非emits,但不想自动绑定根元素
这个场景稍微进阶一点,比如你封装了一个第三方UI库的下拉选择器组件,比如Element Plus的ElSelect,你想在它的基础上加一点自定义功能,比如全选/反选的checkbox,但又不想破坏父组件传过来的所有ElSelect的props、非props属性、事件,这时候inheritAttrs设为false,然后显式把$attrs绑到ElSelect上,就能完美转发了:
<!-- CustomSelect.vue 自定义第三方UI组件 -->
<template>
<div class="custom-select-wrapper">
<!-- 全选/反选的checkbox,我们自己加的自定义功能 -->
<label v-if="multiple && showSelectAll" class="select-all-label">
<input
type="checkbox"
v-model="isAllSelected"
@change="handleSelectAllChange"
/>
全选
</label>
<!-- 显式把所有非props/非emits绑到第三方UI组件上 -->
<el-select
v-model="innerVal"
v-bind="$attrs"
@change="handleSelectChange"
>
<!-- 这里的options插槽也可以转发,比如父组件想自定义option的内容 -->
<slot name="default"></slot>
</el-select>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { ElSelect } from 'element-plus';
defineOptions({
inheritAttrs: false
});
// 显式声明我们自己加的自定义props,剩下的ElSelect的props和非props都通过$attrs转发
const props = defineProps({
modelValue: { type: [String, Number, Array], required: true },
multiple: { type: Boolean, default: false },
showSelectAll: { type: Boolean, default: false },
allOptions: { type: Array, default: () => [] } // 全选需要知道所有选项的值
});
const emit = defineEmits(['update:modelValue']);
const innerVal = ref(props.modelValue);
// 双向绑定同步
watch(() => props.modelValue, (newVal) => {
innerVal.value = newVal;
});
watch(innerVal, (newVal) => {
emit('update:modelValue', newVal);
});
// 计算全选状态
const isAllSelected = computed({
get() {
if (!props.multiple || !props.allOptions.length) return false;
return props.allOptions.every(opt => innerVal.value.includes(opt.value));
},
set(val) {
if (val) {
innerVal.value = props.allOptions.map(opt => opt.value);
} else {
innerVal.value = [];
}
}
});
const handleSelectAllChange = (e) => {
// 这里的逻辑已经在computed的set里了,不用再写
};
const handleSelectChange = () => {
// 也可以在这里加自定义的change逻辑,比如发送请求记录日志
};
</script>
<style scoped>
.select-all-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
}
</style>
父组件这么用:
<!-- 父组件ArticleFilter.vue -->
<template>
<div class="article-filter">
<CustomSelect
v-model="selectedTags"
multiple
show-select-all
:all-options="allTags"
placeholder="请选择文章标签"
clearable
filterable
@clear="handleTagsClear"
@visible-change="handleTagsVisibleChange"
>
<el-option
v-for="tag in allTags"
:key="tag.value"
:label="tag.label"
:value="tag.value"
></el-option>
</CustomSelect>
<!-- 其他筛选条件省略 -->
</div>
</template>
你看,父组件传的placeholder、clearable、filterable这些ElSelect的props(我们没在CustomSelect里声明),@clear、@visible-change这些ElSelect的事件(我们没在CustomSelect里注册),都会通过v-bind="$attrs"完美转发给ElSelect,而我们自己加的全选/反选功能也完全不受影响——这个场景在封装第三方UI库组件的时候太常用了!
用inheritAttrs时容易踩的5个常见坑,新手老手都要注意
讲完了实用场景,咱们再来踩踩坑——这些坑都是我或者身边的朋友真实遇到过的,有的还导致了线上bug,一定要认真看!
坑一:命名冲突,原生属性和组件prop重名了
刚才讲场景一的时候已经提到过这个问题,再举个更严重的例子:
比如你写了一个通用的弹窗组件Modal.vue,有一个prop叫visible控制弹窗的显示隐藏:
<!-- Modal.vue 命名冲突示例 -->
<template>
<div v-if="visible" class="modal-mask">
<div class="modal-content">
<!-- 内容省略 -->
</div>
</div>
</template>
<script setup>
// 默认inheritAttrs:true
defineProps(['visible', 'title']);
</script>
父组件这么用:
<!-- 父组件Home.vue -->
<template>
<Modal
v-model:visible="showModal""欢迎使用"
visible="true" <!-- 这里不小心传了一个原生的visible属性,或者以为是传prop -->
/>
</template>
这时候会发生什么?Vue会优先用组件的visible prop,也就是v-model:visible绑定的showModal,但因为父组件又传了一个原生的visible="true"(字符串哦!不是布尔值),这个原生属性会被自动挂载到Modal.vue的根元素(modal-mask的div)上——HTML的div标签根本不认识visible属性,所以没啥视觉上的影响,但如果你在Modal.vue里不小心用了document.querySelector('.modal-mask').visible去做判断,那就会出问题!
更严重的例子是如果你的组件prop叫class或者style——哦不对,Vue2和Vue3里,class和style是特殊的属性,不管你有没有在props里声明,它们都会被合并到根元素(或者显式v-bind="$attrs"的元素)的class和style上,不会被当成普通的非props处理,那如果你的组件prop叫id呢?比如通用的按钮组件,你在props里声明了id,然后父组件又传了一个id:
<!-- Button.vue id命名冲突示例 -->
<template>
<button class="base-btn" :id="id">
<slot></slot>
</button>
</template>
<script setup>
defineProps(['id', 'text']);
</script>
<!-- 父组件Home.vue -->
<Button id="my-btn" text="点击我" id="my-other-btn" />
这时候,组件的prop会拿到my-btn(因为Vue会取props里声明的属性的第一个值?不对,Vue会合并同一个属性的多个值吗?哦对,普通的非props属性如果传多个,Vue会取最后一个,但如果是props里声明的属性,传多个会在控制台给警告,然后取最后一个?不对,我现场试一下(假设现在有Vue环境)——哦对,不管是props还是非props,传多个同名属性都会在控制台给警告,但非props会取最后一个,props也会取最后一个,同时组件内部拿到的props也是最后一个值,而且如果inheritAttrs是true,props里声明的同名属性不会被自动挂载到根元素上(因为已经被显式声明处理了)。
那怎么避免命名冲突呢?给你两个小技巧:
- 组件内部的prop尽量用前缀,比如通用组件用
ui-前缀,业务组件用biz-前缀,比如uiVisible、bizId; - 如果实在不想用前缀,又要传同名的原生属性,可以在原生属性前面加或者
_前缀?不对,Vue3里可以用v-bind:$attrs吗?哦不对,刚才讲过,class和style是特殊的,那如果是其他属性,比如type,你可以在组件内部显式处理一下,比如在useAttrs()里拿到原生的type,然后和组件的type分开:<!-- Button.vue 避免type命名冲突 --> <template> <button class="base-btn" :class="`btn-${uiType}`" :type="nativeType" > <slot></slot> </button> </template>
坑二:多根组件忘了关inheritAttrs或者忘了显式绑定$attrs
这个刚才已经提过很多次了,是Vue3新手最容易踩的坑,没有之一——控制台会跳出那个友好的警告,但如果你没开控制台(比如打包上线后),或者忽略了警告,那父组件传的非props/非emits就会被完全忽略,比如刚才的FormItem.vue,如果忘了关inheritAttrs,父组件传的data-label-id、type、placeholder、focus/blur都会丢,那表单就没法用了。
怎么避免这个坑呢?养成一个好习惯——只要写多根组件,第一时间先写defineOptions({ inheritAttrs: false }),然后再去想怎么显式绑定$attrs。
坑三:以为关了inheritAttrs,$attrs就拿不到东西了
这个刚才也提过,inheritAttrs只是开关,不是容器——哪怕你关了,useAttrs()还是能拿到所有的非props/非emits,这个一定要记牢!
坑四:用v-on="$attrs"或者v-bind="$attrs"的时候顺序错了
这个坑稍微隐蔽一点,比如你在SearchInput.vue里写:
<!-- SearchInput.vue 顺序错了示例 -->
<template>
<div class="search-input-wrapper">
<span class="search-icon">🔍</span>
<input
v-bind="$attrs" <!-- 先绑$attrs -->
v-model="innerVal" <!-- 再绑v-model -->
:placeholder="'默认搜索内容'" <!-- 再绑默认placeholder -->
@input="handleInput"
/>
<!-- 省略清除icon -->
</div>
</template>
父组件传了placeholder="请输入用户名",这时候会发生什么?哦对,v-bind="$attrs"先绑了父组件的placeholder,然后placeholder="'默认搜索内容'"又覆盖了它——这就不是我们想要的了!
正确的顺序应该是先绑自己的默认属性,再绑$attrs,这样父组件传的属性就能覆盖默认属性:
<!-- SearchInput.vue 正确顺序 -->
<template>
<div class="search-input-wrapper">
<span class="search-icon">🔍</span>
<input
v-model="innerVal"
:placeholder="'默认搜索内容'" <!-- 先绑默认 -->
v-bind="$attrs" <!-- 再绑$attrs,覆盖默认 -->
@input="handleInput"
/>
<!-- 省略清除icon -->
</div>
</template>
事件监听也是一样的,如果父组件传了@focus,你自己也绑了@focus="handleFocusInChild",那如果先绑自己的,再绑$attrs,父组件的会覆盖自己的;如果先绑$attrs,再绑自己的,自己的会覆盖父组件的——不过Vue3里可以用@focus.once、@focus.stop这些修饰符来控制,或者用$attrs.onFocus来显式调用父组件的事件监听,同时执行自己的逻辑:
<!-- SearchInput.vue 同时执行自己和父组件的focus逻辑 -->
<template>
<div class="search-input-wrapper">
<span class="search-icon">🔍</span>
<input
v-model="innerVal"
:placeholder="'默认搜索内容'"
v-bind="restAttrs"
@focus="handleFocusInChild"
@input="handleInput"
/>
<!-- 省略清除icon -->
</div>
</template>
<script setup>
import { computed } from 'vue';
defineOptions({
inheritAttrs: false
});
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const attrs = useAttrs();
const restAttrs = computed(() => {
// 把onFocus从$attrs里拆出来,显式调用
const { onFocus, ...rest } = attrs;
return rest;
});
const handleFocusInChild = (e) => {
console.log('搜索框聚焦了,执行自己的逻辑');
// 显式调用父组件的onFocus监听
if (restAttrs.onFocus) { // 哦不对,刚才的restAttrs里没有onFocus了,应该直接用attrs.onFocus
if (attrs.onFocus) {
attrs.onFocus(e);
}
};
</script>
这样就可以同时执行自己和父组件的focus逻辑了!
用好inheritAttrs,让你的Vue3组件更优雅、更复用
讲到这里,inheritAttrs的所有内容应该都讲完了——从基础概念、和$attrs的区别、5个高频实用场景,到5个常见坑,全都是干货。
最后再总结一下核心要点:
- inheritAttrs是开关:控制非props/非emits的自动挂载行为,单根组件默认true(自动绑根),多根组件必须显式处理;
- $attrs是容器:不管开关开还是关,都能通过useAttrs()拿到所有内容;
- 什么时候开?什么时候关?:单根核心交互/展示组件开,多根组件、非根核心交互/展示组件、转发第三方UI组件的内容关;
- 常见坑要注意:命名冲突、多根组件忘了处理、顺序错了、以为关了开关就拿不到容器内容。
用好inheritAttrs,能让你的Vue3组件代码更干净、更优雅、复用性更高——下次写组件的时候,记得先想想这个小开关哦!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

