背景
vue3 中有一个很有意思的特性,就是属性透传,这个特性我们经常会使用到,但往往不会注意到它,直到在实际工作中出现了一个警告,才让我注意到它:
这个警告的大致意思就是,在子组件中,由于子组件有多个节点或者使用了文本根节点导致非 props 接收的属性无法自动继承,我看到这个警告时,也是一脸疑惑,并不知道哪里出了问题,(可能是我当初在学习 vue 时忽略了这个知识点),在经过查阅百度和文档,才彻底了解了它的作用,故写下这篇博客记录一下 vue3 中的属性透传。
基本概念
首先看官方文档的解释:
"透传 attribute"指的是传递给一个组件,却没有被该组件声明为
props
或emits
的attribute
或者v-on
事件监听器。最常见的例子就是class
、style
和id
。当一个组件以单个元素为根作渲染时,透传的attribute
会自动被添加到根元素上。
官网介绍的非常清楚:当我们给子组件传递一个属性或者事件时,子组件由于没有使用 pros
和 emits
接收,于是该属性或属性就被自动传递到子组件的根元素节点上,具体可以看这个例子:
html
// parents.vue
<template>
我是父组件
<Children class="children" style="color: red;" name="aaa" age="1"> </Children>
</template>
<script setup lang="ts">
import Children from "./children.vue";
</script>
<style scoped></style>
html
// children.vue
<template>
<div>我是子组件</div>
</template>
<script setup lang="ts">
defineProps({
name: String,
});
</script>
<style scoped></style>
我们在父组件中给子组件传递了 class
、style
、name
、age
等属性和事件,而在子组件中,只在props
中声明了name
属性,于是我们打开浏览器看看效果:
可以看到,class
、age
、style
均传递到了子组件的根节点上,而被props
接收的 name
属性则没有被传递到子组件的根节点上,这就是属性透传。仔细想想我们给组件定义的 style
、class
、id
等属性是不是都用了这个特性。
属性的自动合并
子组件除了可以自动继承父组件传递过来的值以外,针对style
和class
属性子组件还可以进行属性的合并,而对于其他自定义的属性则会被传递过来的属性值覆盖,具体可以看以下代码:
html
// parents.vue
<template>
我是父组件
<Children class="children" style="color: red;" name="aaa"> </Children>
</template>
<script setup lang="ts">
import Children from "./children.vue";
</script>
<style scoped></style>
html
// children.vue
<template>
<div class="children1" style="background-color: blue" name="bbb">
我是子组件
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
在父组件中我们给子组件传递了 class
、style
、name
属性,而在子组件中,我们同样给根节点定义了这三个属性,看一下效果:
此时子组件的根节点的class
和style
属性会自动合并,而被自定义name
属性则被父组件传递过来的值覆盖。
如果我们给父组件和子组件同时定义一个click
事件,那么会同时触发还是只触发一个呢?
html
// parents.vue
<template>
我是父组件
<Children @click="handleClick"></Children>
</template>
<script setup lang="ts">
import Children from "./children.vue";
function handleClick() {
console.log("p-click");
}
</script>
<style scoped></style>
html
// children.vue
<template>
<div @click="handleClick">我是子组件</div>
</template>
<script setup lang="ts">
function handleClick() {
console.log("c-click");
}
</script>
<style scoped></style>
看效果:
答案就是都会触发,并且触发顺序为先子组件后父组件。
以上就是属性和事件的合并。
深层组件透传
如果我们将子组件的根节点再替换为一个组件,类似这样:
html
// children.vue
<template>
<MybButton></MybButton>
</template>
<script setup lang="ts">
import MybButton from "./MyButton.vue";
</script>
<style scoped></style>
html
// MybButton.vue
<template>
<div>我是孙组件</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>
此时,子组件的透传属性,会原封不动的传递给孙组件:
这里要注意两点:
- 如果父组件传递给子组件的属性或事件被
props
和emits
接收了, 那么将不会被透传给孙组件,可以理解为props
和emits
声明过的属性和事件被子组件消费了 - 传递的属性若符合声明,也同样可以作为
props
和emits
传递给孙组件
禁用属性透传
如果不想一个组件自动地继承传递过来的属性,可以在组件选项中设置 inheritAttrs: false
。从 vue3.3 开始也可以直接在 defineOptions
中定义:
html
<script setup>
defineOptions({
inheritAttrs: false,
});
</script>
这样属性的就不会自动传递到子组件的根节点上了。
访问透传属性
首先,在模板中,我们可以使用 $attrs
来获取未被 props
和 emits
接收的属性,即透传的属性:
html
// parents.vue
<template>
我是父组件
<Children class="children" name="aaa"> </Children>
</template>
<script setup lang="ts">
import Children from "./children.vue";
</script>
<style scoped></style>
html
// children.vue
<template>
<div>{{$attrs}}</div>
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false,
});
</script>
<style scoped></style>
在父组件中,我们同样给子组件传递两个属性,在子组件中我们禁用自动继承,来看 $attrs
包含哪些属性:
可以看到 $attrs 就是一个包含了透传属性的对象,这里也有两点需要注意的:
- 和
props
有所不同,透传属性在保留了它们原始的大小写,所以像foo-bar
这样的一个属性名需要通过$attrs['foo-bar']
来访问。 - 像
@click
这样的一个v-on
事件监听器则需要使用$attrs.onClick
来访问。
而在 javascript
中, 可以使用<script setup>
中的 useAttrs()
API来访问一个组件的所有透传属性:
html
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs)
</script>
打印一下, 可以看到 useAttrs() 得到的同样为一个包含了透传属性的对象:
如果没有使用 <script setup>
,attrs
会作为 setup()
上下文对象的一个属性暴露:
js
export default {
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs
console.log(ctx.attrs)
}
}
这里需要注意的是,透传属性对象并不是响应式的 (考虑到性能因素),不能通过侦听器去监听它的变化。如果需要响应性,可以使用 prop
。或者也可以使用 onUpdated()
使得在每次更新时获取最新的 attrs
$attrs
的显示绑定
我们都知道 在vue3中, 组件可以同时有多个根节点,那么如果子组件有多个根节点,这时的透传属性则不知道该传递给哪个节点,就会出现文章一开始的警告,如要消除这个警告,则要显示的绑定透传属性,可以使用 v-bind='$attrs'
来指定绑定到哪个节点:
html
// children.vue
<template>
<header>...</header>
<main v-bind="$attrs"></main>
<footer>...</footer>
</template>
这样就被绑定到了 main
元素上,而不会出现警告了。
同样的,如果我们想绑定到非根节点上,那么我们需要将自动透传禁用,然后在想要绑定的节点上使用 v-bind
来绑定透传属性:
html
// children.vue
<template>
<div>
<span v-bind="$attrs"></span>
</div>
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false,
});
</script>
<style scoped></style>
到这里 vue3 中的属性透传就介绍完了,希望对你有所帮助。