人人都会AI编程

$attrs / $listeners:属性透传与二次封装组件场景

更新时间:2026-07-10

在开发中,经常需要对原生 HTML 元素或第三方组件进行二次封装。比如你不想每个地方都重复写 <input class="my-input" placeholder="请输入..."> ,而是希望统一封装一个 <MyInput> 组件,既保留原生 input 的所有能力和事件,又可以加上统一的样式、校验等自定义逻辑。

这种场景的核心需求是:把父组件传进来的多余属性和事件原封不动地“透传”给包裹的底层元素。Vue 提供的 $attrs(以及 Vue 2 时代的 $listeners)就是专门解决这个问题的。

$attrs 里有什么?

$attrs 是一个组件实例上的对象,包含了父组件传递下来,但未在当前组件的 propsemits 中声明接收的所有属性和事件。包括:

  • 普通的 HTML 属性,如 classstyleidplaceholderdisabled 等。
  • 自定义的事件监听器,如 @click@focus@input 等。(在 Vue 3 中,事件监听器也包含在 $attrs 内)
  • 非 prop 的自定义数据属性,如 data-*

Vue 3 与 Vue 2 的重要区别
在 Vue 2 中,$attrs 不包含事件监听器,事件监听器单独存放在 $listeners 里。你往往需要同时写 v-bind="$attrs"v-on="$listeners"
Vue 3 做了简化,把所有东西都合并到了 $attrs 中。事件监听器也作为 onXxx 形式的属性存在其中(例如 onClickonFocus)。

最简单的透传:v-bind="$attrs"

在模板中,可以直接用 v-bind="$attrs" 把当前组件接收到的所有额外属性/事件一次性绑定到内部元素上:

<!-- MyInput.vue —— 一个二次封装的输入框 -->
<template>
  <div class="my-input-wrapper">
    <label v-if="label">{{ label }}</label>
    <!-- 核心透传:将所有外部属性/事件转给原生 input -->
    <input v-bind="$attrs" />
  </div>
</template>

<script setup>
// 只声明当前组件特有的 props
defineProps({
  label: String
})
</script>

父组件使用时,就可以像用普通 <input> 一样用 <MyInput>

<template>
  <MyInput
    label="用户名"
    v-model="username"
    placeholder="请输入用户名"
    class="custom-input"
    @focus="handleFocus"
  />
</template>

这个时候,placeholderclassonFocus(即 @focus)以及 v-model 绑定的 onUpdate:modelValue 都不会被 MyInputpropsemits 声明捕获,它们会全部进入 $attrs,然后通过 v-bind="$attrs" 传递到内部的 <input /> 上。你无需在 MyInput 里为每一个可能用到的原生属性声明 props,这确保了封装的通用性和灵活性。

处理 v-model 透传

上例中 v-model 默认绑定 modelValue 属性和 onUpdate:modelValue 事件。由于我们没有在 MyInput 里声明 modelValue 为 prop,它也会被放入 $attrs 并透传给内部 <input>。但 v-model 需要让内部 input 的输入事件能更新这个值,原生 input 的 v-model 监听的是 input 事件和 value 属性,所以需要确保透传后能正常用。这样做通常已经能够工作,因为 v-model 在 input 上等价于 :value="modelValue"@input="...",透传后内部 input 会收到这些绑定。

但如果你希望对输入值做更精细的控制(比如加一个 trim 处理),可以显式声明 modelValue 为 prop,并手动转发 @update:modelValue

<template>
  <div class="my-input-wrapper">
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      v-bind="$attrs"
    />
  </div>
</template>

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

注意 v-bind="$attrs" 里也可能包含 onUpdate:modelValue,如果你自己手动发射了事件,可能会出现重复调用。因此显式处理 v-model 时,最好将 modelValueonUpdate:modelValue$attrs 中排除,可以使用 computed 过滤,或者在模板中手动绑定除 modelValue 以外的属性。更简单的是,把 v-bind="$attrs" 放在 :value@input 之后,以保持正确的绑定顺序;重复绑定同一个值通常不会有问题,但要注意。

控制透传行为:inheritAttrs

默认情况下,$attrs 中所有的属性会被自动继承到组件的根元素上(即模板最外层的那个元素)。在上面的例子中,<div class="my-input-wrapper"> 是根元素,这意味着如果父组件传了 class="custom-input",它会同时被应用到根 div 和内部 input 上。这通常不是我们想要的效果——我们只想让 input 接收这些属性。

通过设置 inheritAttrs: false 可以禁用自动继承,然后手动用 v-bind="$attrs" 显式选择要绑定的元素:

<template>
  <div class="my-input-wrapper">
    <input v-bind="$attrs" />
  </div>
</template>

<script setup>
defineOptions({
  inheritAttrs: false  // 禁止自动挂到根 div 上
})
</script>

现在,父组件传的 classstyle 等只会被透传到 <input> 上,而不会污染外层的 <div>。在二次封装 UI 组件时,这个设置几乎是必须的。

<script setup> 中使用 $attrs

在组合式 API 的 <script setup> 中,可以用 useAttrs() 获取 $attrs 对象:

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
// 你可以在这里对 attrs 做过滤、计算等操作
</script>

useAttrs() 返回的是响应式的对象,但需要注意它不是深度响应式的,只跟踪引用变化。你可以在需要时将其转换为普通对象用于逻辑判断。

二次封装组件的典型模式

利用 $attrs 和插槽,可以快速构建“薄封装”组件:

  1. 禁用自动继承(inheritAttrs: false)。
  2. 只声明当前组件特有的 props(如 labelerror)。
  3. v-bind="$attrs" 把剩余所有属性/事件透传给目标子组件或原生元素。
  4. <slot> 支持内容分发。

这样封装出的组件既保持了与原组件/原生元素几乎相同的 API,又平添了新的业务特性,极大减少了重复代码和后续维护成本。