Code前端首页关于Code前端联系我们

Vue3的inheritAttrs属性到底是干嘛的?用它能解决我哪些实际开发痛点?

terry 59分钟前 阅读数 17 #Vue

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>

这时候你再打开元素检查器:

  1. data-testid="my-pwd-input"绑到了外层的label上(显式拆出来的);
  2. type="password"、placeholder="请输入密码"、focus/blur事件绑到了input上(v-bind restAttrs);
  3. 控制台没有警告,因为我们显式指定了所有非根绑定的去处;
  4. 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里,classstyle是特殊的属性,不管你有没有在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里声明的同名属性不会被自动挂载到根元素上(因为已经被显式声明处理了)。

那怎么避免命名冲突呢?给你两个小技巧:

  1. 组件内部的prop尽量用前缀,比如通用组件用ui-前缀,业务组件用biz-前缀,比如uiVisiblebizId
  2. 如果实在不想用前缀,又要传同名的原生属性,可以在原生属性前面加或者_前缀?不对,Vue3里可以用v-bind:$attrs吗?哦不对,刚才讲过,classstyle是特殊的,那如果是其他属性,比如type,你可以在组件内部显式处理一下,比如在useAttrs()里拿到原生的type,然后和组件的type分开:
    <!-- Button.vue 避免type命名冲突 -->
    <template>
    <button 
     class="base-btn" 
     :class="`btn-${uiType}`"
     :type="nativeType"
    >
     <slot></slot>
    </button>
    </template>
``` 刚才的template里漏了v-bind restAttrs,现在补上完整的: ```html ``` 这样就完美避免了命名冲突!

坑二:多根组件忘了关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个常见坑,全都是干货。

最后再总结一下核心要点:

  1. inheritAttrs是开关:控制非props/非emits的自动挂载行为,单根组件默认true(自动绑根),多根组件必须显式处理;
  2. $attrs是容器:不管开关开还是关,都能通过useAttrs()拿到所有内容;
  3. 什么时候开?什么时候关?:单根核心交互/展示组件开,多根组件、非根核心交互/展示组件、转发第三方UI组件的内容关;
  4. 常见坑要注意:命名冲突、多根组件忘了处理、顺序错了、以为关了开关就拿不到容器内容。

用好inheritAttrs,能让你的Vue3组件代码更干净、更优雅、复用性更高——下次写组件的时候,记得先想想这个小开关哦!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门