Vue 3 透传 Attributes 第四章:$attrs 对象的深度理解与应用
扫描 关注或者微信搜一搜:编程智域 前端至全栈交流与成长
一、$attrs 对象的结构与内容
$attrs 是一个包含组件所有未声明的 attributes 的对象。具体来说,以下 attributes 会进入 $attrs:
未在
props中声明的属性未在
emits中声明的事件监听器所有 HTML 标准属性(class、style、id 等)
自定义属性(data-、aria- 等)
不包括:
已在
props中声明的属性已在
emits中声明的事件(Vue 3 中会从 $attrs 中移除)
1.2 $attrs 的结构示例
<!-- 父组件 -->
<template>
<ChildComponent
title="标题"
class="my-class"
id="my-id"
data-foo="bar"
@click="handleClick"
@custom-event="handleCustom"
/>
</template><!-- 子组件 -->
<script setup>
import { onMounted, useAttrs } from 'vue'
const attrs = useAttrs()
onMounted(() => {
console.log('$attrs:', attrs)
// 输出:
// {
// class: 'my-class',
// id: 'my-id',
// 'data-foo': 'bar',
// onClick: ƒ,
// onCustomEvent: ƒ
// }
})
</script>
<template>
<div v-bind="$attrs">内容</div>
</template>注意:
title在 props 中声明,不会出现在$attrs中class、id、data-foo会出现在$attrs中@click和@custom-event会转换为onClick和onCustomEvent
1.3 事件监听器的命名规则
在 $attrs 中,事件监听器以 on 开头,使用 PascalCase 命名:
<!-- 父组件 -->
<ChildComponent
@click="handleClick"
@custom-event="handleCustom"
@update:model-value="handleUpdate"
/><!-- 子组件 -->
<script setup>
const attrs = useAttrs()
console.log(Object.keys(attrs))
// ['onClick', 'onCustomEvent', 'onUpdate:modelValue']
</script>二、在模板中访问和使用 $attrs
2.1 直接使用 $attrs
在模板中,可以直接使用 $attrs 而无需导入:
<template>
<div v-bind="$attrs">
<slot />
</div>
</template>2.2 访问特定属性
<template>
<div
:id="$attrs.id"
:class="$attrs.class"
:data-foo="$attrs['data-foo']"
>
<slot />
</div>
</template>2.3 条件渲染
<template>
<div>
<label v-if="$attrs.required" class="required">*</label>
<input v-bind="$attrs" />
</div>
</template>2.4 动态组件
<template>
<component
:is="componentType"
v-bind="$attrs"
>
<slot />
</component>
</template>
<script setup>
defineProps({
componentType: {
type: String,
default: 'input'
}
})
</script>三、在 script setup 中获取 $attrs
3.1 使用 useAttrs 导入
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs.class)
console.log(attrs.id)
</script>3.2 响应式特性
useAttrs() 返回的对象是响应式的,当父组件传递的 attributes 变化时,attrs 会自动更新。
<script setup>
import { useAttrs, watch } from 'vue'
const attrs = useAttrs()
// 监听整个 attrs 对象
watch(attrs, (newAttrs) => {
console.log('attrs 变化:', newAttrs)
}, { deep: true })
// 监听特定属性
watch(
() => attrs.class,
(newClass) => {
console.log('class 变化:', newClass)
}
)
</script>3.3 在计算属性中使用
<script setup>
import { useAttrs, computed } from 'vue'
const attrs = useAttrs()
const boundAttrs = computed(() => ({
id: attrs.id,
class: [attrs.class, 'custom-class'],
style: attrs.style
}))
</script>
<template>
<div v-bind="boundAttrs">
<slot />
</div>
</template>3.4 注意事项:解构失去响应式
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
// 错误:解构会失去响应式
const { class: className, id } = attrs
// 正确:使用计算属性或 getter 函数
const className = computed(() => attrs.class)
const getId = () => attrs.id
</script>四、在 Options API 中访问 $attrs
4.1 使用 this.$attrs
<script>
export default {
created() {
console.log(this.$attrs)
},
methods: {
handleUpdate() {
console.log(this.$attrs.class)
}
}
}
</script>4.2 与 setup 混用
<script>
export default {
props: ['title'],
created() {
console.log(this.$attrs)
}
}
</script>
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
// attrs 和 this.$attrs 是同一个对象
</script>五、高级应用技巧
5.1 属性过滤和转换
<!-- SmartInput.vue -->
<template>
<div class="input-wrapper">
<input
class="input"
v-bind="inputAttrs"
/>
<span v-if="error" class="error">{{ error }}</span>
</div>
</template>
<script setup>
import { useAttrs, computed } from 'vue'
const props = defineProps({
error: String
})
const attrs = useAttrs()
// 过滤出需要绑定到 input 的属性
const inputAttrs = computed(() => {
const { class: className, style, ...rest } = attrs
return {
class: ['input', className].filter(Boolean).join(' '),
style,
...rest
}
})
</script>5.2 属性代理
将 attributes 代理到子组件的不同部分:
<!-- SplitAttrs.vue -->
<template>
<div class="container">
<header v-bind="headerAttrs">
<slot name="header" />
</header>
<main v-bind="mainAttrs">
<slot />
</main>
<footer v-bind="footerAttrs">
<slot name="footer" />
</footer>
</div>
</template>
<script setup>
import { useAttrs, computed } from 'vue'
const attrs = useAttrs()
const headerAttrs = computed(() => ({
class: attrs.headerClass,
style: attrs.headerStyle
}))
const mainAttrs = computed(() => ({
id: attrs.id,
class: attrs.mainClass,
'data-section': attrs['data-section']
}))
const footerAttrs = computed(() => ({
class: attrs.footerClass
}))
</script>使用:
<SplitAttrs
id="page"
header-class="page-header"
main-class="page-main"
footer-class="page-footer"
data-section="home"
>
<template #header>头部</template>
主内容
<template #footer>底部</template>
</SplitAttrs>5.3 条件透传
根据条件决定是否透传某些属性:
<!-- ConditionalAttrs.vue -->
<template>
<component
:is="as"
v-bind="boundAttrs"
>
<slot />
</component>
</template>
<script setup>
import { useAttrs, computed } from 'vue'
const props = defineProps({
as: {
type: String,
default: 'div'
},
allowId: {
type: Boolean,
default: true
},
allowClass: {
type: Boolean,
default: true
}
})
const attrs = useAttrs()
const boundAttrs = computed(() => {
const result = {}
if (props.allowId && attrs.id) {
result.id = attrs.id
}
if (props.allowClass && attrs.class) {
result.class = attrs.class
}
// 始终透传 data-* 和 aria-* 属性
Object.keys(attrs).forEach(key => {
if (key.startsWith('data-') || key.startsWith('aria-')) {
result[key] = attrs[key]
}
})
return result
})
</script>5.4 属性合并策略
<!-- MergeAttrs.vue -->
<template>
<div v-bind="mergedAttrs">
<slot />
</div>
</template>
<script setup>
import { useAttrs, computed } from 'vue'
const props = defineProps({
baseClass: String,
baseStyle: Object
})
const attrs = useAttrs()
const mergedAttrs = computed(() => {
const result = { ...attrs }
// 合并 class
if (props.baseClass || attrs.class) {
const classes = []
if (props.baseClass) classes.push(props.baseClass)
if (attrs.class) classes.push(attrs.class)
result.class = classes.join(' ')
}
// 合并 style
if (props.baseStyle || attrs.style) {
result.style = {
...props.baseStyle,
...attrs.style
}
}
return result
})
</script>5.5 监听 attrs 变化执行副作用
<!-- WatchAttrs.vue -->
<template>
<div v-bind="$attrs">
<slot />
</div>
</template>
<script setup>
import { useAttrs, watch, onMounted, onUnmounted } from 'vue'
const attrs = useAttrs()
let cleanupFn = null
// 监听 attrs 变化
watch(
attrs,
(newAttrs, oldAttrs) => {
// 清理旧的副作用
if (cleanupFn) {
cleanupFn()
}
// 根据新 attrs 执行副作用
if (newAttrs['data-auto-focus']) {
const el = document.getElementById(newAttrs.id)
if (el) {
el.focus()
cleanupFn = () => el.blur()
}
}
},
{ deep: true }
)
onUnmounted(() => {
if (cleanupFn) {
cleanupFn()
}
})
</script>六、实际应用场景
6.1 可配置的图标组件
<!-- Icon.vue -->
<template>
<component
:is="as"
class="icon"
:class="iconClass"
v-bind="boundAttrs"
>
<slot>
<svg v-if="iconType" viewBox="0 0 24 24">
<path :d="iconPaths[iconType]" />
</svg>
</slot>
</component>
</template>
<script setup>
import { useAttrs, computed } from 'vue'
const props = defineProps({
as: {
type: String,
default: 'i'
},
iconType: String,
size: {
type: String,
default: 'medium'
}
})
const attrs = useAttrs()
const iconClass = computed(() => ({
[`icon-${props.size}`]: true
}))
const iconPaths = {
home: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z',
user: 'M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z',
settings: 'M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z'
}
const boundAttrs = computed(() => {
const { class: className, style, ...rest } = attrs
return {
...rest,
'aria-hidden': attrs['aria-hidden'] ?? true
}
})
</script>
<style scoped>
.icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon svg {
width: 1em;
height: 1em;
fill: currentColor;
}
.icon-small {
font-size: 12px;
}
.icon-medium {
font-size: 16px;
}
.icon-large {
font-size: 24px;
}
</style>使用:
<template>
<Icon icon-type="home" size="large" class="home-icon" />
<Icon icon-type="user" @click="handleUserClick" />
<Icon as="button" icon-type="settings" aria-label="设置" />
</template>6.2 高阶组件:带日志的属性透传
<!-- WithLogging.vue -->
<template>
<component
:is="component"
v-bind="loggedAttrs"
>
<slot />
</component>
</template>
<script setup>
import { useAttrs, computed, watch } from 'vue'
const props = defineProps({
component: {
type: String,
required: true
},
logChanges: {
type: Boolean,
default: true
}
})
const attrs = useAttrs()
const loggedAttrs = computed(() => {
if (props.logChanges) {
console.log('透传的 attributes:', attrs)
}
return attrs
})
watch(attrs, (newAttrs, oldAttrs) => {
if (props.logChanges) {
console.log('Attributes 变化:')
console.log('旧:', oldAttrs)
console.log('新:', newAttrs)
}
}, { deep: true })
</script>6.3 动态表单字段组件
<!-- DynamicField.vue -->
<template>
<div class="field">
<label v-if="label" :for="fieldId">{{ label }}</label>
<component
:is="fieldType"
:id="fieldId"
class="field-control"
v-bind="fieldAttrs"
>
<slot />
</component>
</div>
</template>
<script setup>
import { useAttrs, computed } from 'vue'
const props = defineProps({
label: String,
fieldType: {
type: String,
default: 'input'
},
fieldId: String
})
const attrs = useAttrs()
const fieldAttrs = computed(() => {
const { class: className, ...rest } = attrs
return {
class: ['field-control', className].filter(Boolean).join(' '),
...rest
}
})
</script>
<style scoped>
.field {
margin-bottom: 16px;
}
.field label {
display: block;
margin-bottom: 4px;
font-weight: 500;
}
.field-control {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>使用:
<template>
<DynamicField
label="用户名"
field-type="input"
field-id="username"
type="text"
placeholder="请输入用户名"
required
/>
<DynamicField
label="性别"
field-type="select"
field-id="gender"
>
<option value="male">男</option>
<option value="female">女</option>
</DynamicField>
<DynamicField
label="备注"
field-type="textarea"
field-id="note"
rows="4"
/>
</template>七、性能优化
7.1 使用计算属性缓存
<script setup>
import { useAttrs, computed } from 'vue'
const attrs = useAttrs()
// 推荐:使用计算属性缓存
const processedAttrs = computed(() => {
const result = {}
Object.keys(attrs).forEach(key => {
if (key.startsWith('data-')) {
result[key] = attrs[key]
}
})
return result
})
</script>7.2 避免不必要的 watch
<script setup>
import { useAttrs, watch } from 'vue'
const attrs = useAttrs()
// 不推荐:深度监听整个 attrs
watch(attrs, handler, { deep: true })
// 推荐:只监听特定属性
watch(
() => attrs.class,
handler
)
</script>7.3 清理副作用
<script setup>
import { useAttrs, watch, onUnmounted } from 'vue'
const attrs = useAttrs()
let cleanup = null
watch(
() => attrs['data-tooltip'],
(tooltip) => {
if (cleanup) cleanup()
if (tooltip) {
// 创建 tooltip
cleanup = () => {
// 销毁 tooltip
}
}
},
{ immediate: true }
)
onUnmounted(() => {
if (cleanup) cleanup()
})
</script>课后 Quiz
问题 1:如何在 script setup 中正确访问 $attrs?
答案:使用 useAttrs() 导入:
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>问题 2:为什么解构 $attrs 会失去响应式?
答案:useAttrs() 返回的是一个响应式代理对象,解构会破坏代理关系,导致失去响应式。应该使用计算属性或 getter 函数来访问特定属性。
问题 3:如何在 Options API 和 Composition API 混用时访问 $attrs?
答案:
Options API 中使用
this.$attrsComposition API 中使用
useAttrs()两者引用的是同一个对象
常见报错解决方案
报错 1:useAttrs 未定义
现象:在 script setup 中使用 $attrs 报错。
原因:没有导入 useAttrs。
解决方案:
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>报错 2:解构后属性不更新
现象:解构 $attrs 后,属性变化不响应。
原因:解构失去响应式。
解决方案:
<script setup>
import { useAttrs, computed } from 'vue'
const attrs = useAttrs()
// 错误
// const { class: className } = attrs
// 正确
const className = computed(() => attrs.class)
</script>报错 3:watch attrs 不触发
现象:监听 attrs 对象变化不触发。
原因:需要设置 deep: true 或监听具体属性。
解决方案:
<script setup>
import { useAttrs, watch } from 'vue'
const attrs = useAttrs()
// 监听整个对象
watch(attrs, handler, { deep: true })
// 或监听具体属性
watch(() => attrs.class, handler)
</script>参考链接
余下文章内容请点击跳转至 个人博客页面 或者 扫描 关注或者微信搜一搜:编程智域 前端至全栈交流与成长,阅读完整的文章:
往期文章归档
</details>
免费好用的热门在线工具
评论
发表评论