一句话:二次封装 UI 库按钮时,别一个个 props 转发——用
$attrs/$listeners(Vue 3 合并为$attrs)把「没声明的属性与事件」原样透传给内层组件。
写在前面
业务里多处用到同一套渐变按钮、统一宽度的表单控件时,会在 Vant / Element 外包一层。若每个 disabled、loading、@click 都手写转发,封装层会迅速膨胀。透传让外层用法与直接用 UI 库几乎一致。
读完你能:在 Vue 2 用 $attrs + $listeners;在 Vue 3 用 inheritAttrs: false + v-bind="$attrs"。
核心内容
Vue 2:attrs 与 listeners
| 对象 | 包含内容 | 不包含 |
|---|---|---|
$attrs |
父组件 v-bind 传入、但未在 props 声明的属性 |
class、style(挂在根上)、已声明的 props |
$listeners |
父组件 v-on 绑定的事件(除 .native) |
— |
默认情况下,未声明的 attribute 会自动落到根元素。若根不是目标组件(例如外层包了 div),就需要手动透传:
vue
<template>
<van-button
class="gradient-btn"
v-bind="$attrs"
v-on="$listeners"
:color="color"
:style="{ width }"
>
<span :style="{ color: textColor }">
<slot />
</span>
</van-button>
</template>
<script>
export default {
name: 'GradientBtn',
inheritAttrs: false, // 可选:阻止 attrs 落到错误根节点
props: {
color: {
type: String,
default: 'linear-gradient(180deg, #FFA300 0%, #FF842F 100%)'
},
width: { type: String, default: '135px' },
textColor: { type: String, default: '#fff' }
}
}
</script>
父组件用法不变:
vue
<GradientBtn disabled loading @click="submit">提交</GradientBtn>
disabled、loading、@click 经透传进入内层 van-button。
Vue 3:变化对照
| Vue 2 | Vue 3 |
|---|---|
$listeners |
合并进 $attrs(事件以 onClick 等形式存在) |
.sync |
v-model:propName |
$attrs 不含 class/style(在根上) |
class/style 也在 $attrs 中,需决定是否透传 |
Vue 3 示例:
vue
<script setup>
defineOptions({ inheritAttrs: false })
defineProps({
color: { type: String, default: 'linear-gradient(180deg, #FFA300 0%, #FF842F 100%)' },
width: { type: String, default: '135px' }
})
</script>
<template>
<van-button class="gradient-btn" v-bind="$attrs" :color="color" :style="{ width }">
<slot />
</van-button>
</template>
事件无需单独 v-on="$listeners",@click 等已在 $attrs 里。
透传 vs 显式 props
| 方式 | 优点 | 缺点 |
|---|---|---|
透传 $attrs |
封装薄、API 与 UI 库一致 | 类型提示弱、难文档化 |
| 显式声明 props | TS 友好、语义清晰 | 维护成本高 |
团队组件库可 显式声明常用 props + 透传其余。
踩坑
- 重复绑定:外层
:disabled="false"与内层 props 同名会冲突,封装层应用v-bind合并顺序控制优先级。 inheritAttrs: false忘记手动 bind:属性会丢失。- Vue 2 根元素不是目标组件时,class 可能落在外层 div,样式选择器失效。
- Vue 3 中
$attrs含class,内外层 class 合并需用$attrs.class或拆分。 - 透传过多导致 难以追踪数据来源,关键业务字段仍建议显式 props。
小结
- 二次封装 UI 库的核心是「少写胶水代码」。
- Vue 2:
v-bind="$attrs"+v-on="$listeners"。 - Vue 3:仅
v-bind="$attrs",配合inheritAttrs: false。 - 重要 props 显式声明,其余透传。


全部评论(0)