前言
Vue 3 现在正式支持了多根节点的组件,也就是片段!
Vue 2.x 遵循单根节点组件的规则,即一个组件的模板必须有且仅有一个根元素。
为了满足单根节点的要求,开发者会将原本多根节点的内容包裹在一个<div>
元素中:
html
<!-- Layout.vue -->
<template>
<div>
<h1>标题</h1>
<p>段落</p>
</div>
</template>
这是因为Vue 的编译器在解析组件模板时,是基于单根节点的树形结构进行处理的。如果存在多个根节点,编译器无法明确地构建组件的虚拟 DOM 结构。
因此,在Vue 2.x中,父组件在使用子组件时,写在子组件上的 class
、style
和 id
等属性会直接传递到子组件的根元素上。
Vue 3.x 打破了 Vue 2.x 中组件模板必须有且仅有一个根元素的限制,现在组件可以包含多个根节点。
html
<template>
<h1>标题</h1>
<p>段落</p>
</template>
当组件存在多个根节点时,在父组件中给该组件传递属性(attribute)就需要明确指定这些属性应该绑定到哪个根节点上。如果不进行显式指定,Vue 无法确定属性的归属。
html
<template>
<ChildComponent>
<template v-slot:default="{ attrs }">
<div v-bind="attrs">这是一个 div 根节点</div>
<p>这是一个 p 根节点</p>
<span>这是一个 span 根节点</span>
</template>
</ChildComponent>
</template>
在示例中,通过v-slot:default="{attrs}"
获取到ChildComponent.vue
通过插槽传递给父组件的所有属性(存储在attrs
中),然后使用v-bind="attrs"
将这些属性显式地绑定到其中一个根节点<div>
上。
Attributes 继承
"透传 attribute"指的是传递给一个组件,却没有被该组件声明为 props
或 emits
的 attribute 或者 v-on
事件监听器。
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。
最常见的例子就是 class
、style
和 id
。
子组件ChildComponent.vue
的模板内容如下:
html
<div>这是子组件的div</div>
在父组件使用子组件ChildComponent.vue
,并且传入了 class
、style
和 id
:
html
<template>
<div>
<h1>父组件</h1>
<ChildComponent class="child-div" id="child-div"
style="font-size: 20px; color: brown;" />
</div>
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
</script>
渲染出的DOM结果是:
html
<div class="child-div" id="child-div" style="font-size: 20px; color: brown">
这是子组件的div
</div>
<ChildComponent>
并没有将 class
、style
和 id
声明为它所接受的 prop,所以 class
、style
和 id
被视作透传 attribute,自动透传到了 <ChildComponent>
的根元素上。
当 style
属性透传到子组件的根元素后,它的生效方式与直接在 HTML 元素上设置 style
属性是一样的:
自动合并 class
或 style
如果一个子组件的根元素已经有了 class
或 style
attribute,它会和从父组件上继承的值合并。
给子组件ChildComponent.vue
加上class
、style
、id
属性:
html
<div
class="child-box"
id="child-box"
style="padding: 15px; background-color: #f8f8f8"
>
这是子组件的div
</div>
渲染出的DOM结果是:
html
<div
class="child-box child-div"
id="child-div"
style="
padding: 15px;
background-color: rgb(248, 248, 248);
font-size: 20px;
color: brown;
"
>
这是子组件的div
</div>
子组件的class
、style
属性值 和 从父组件上继承的值合并,子组件的 id
属性值被从父组件继承的 id
属性值覆盖。
页面渲染结果如下图:
v-on
监听器继承
子组件ChildComponent.vue
的模板内容如下:
html
<div @click="console.log('子组件的点击事件被触发了')"> 这是子组件的div </div>
在父组件中,给<ChildComponent>
绑定一个点击事件:
html
<template>
<div class="home-wrap">
<h1>父组件</h1>
<ChildComponent @click="console.log('在父组件中,给子组件绑定的点击事件被触发了!')"/>
</div>
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
</script>
父组件中绑定的click
监听器会被添加到 <ChildComponent>
的根元素:子组件的 <div>
元素之上。
子组件的<div>
元素自身也通过 v-on
绑定了一个事件监听器。
当点击子组件的<div>
元素,子组件的click
监听器和从父组件继承的监听器都会被触发:
深层组件继承
有些情况下一个组件会在根节点上渲染另一个组件。
当ChildComponent.vue
的根节点渲染的是另一个组件GrandChild.vue
时:
html
<!-- ChildComponent.vue 的模板,只是渲染另一个组件:<GrandChild /> -->
<template>
<GrandChild />
</template>
此时 <ChildComponent>
接收的透传 attribute 会直接继续传给 <GrandChild>
。
注意:
-
透传的 attribute 不会包含
<ChildComponent>
上声明过的 props 或是针对emits
声明事件的v-on
侦听函数,换句话说,声明过的 props 和侦听函数被<ChildComponent>
"消费"了。 -
透传的 attribute 若符合声明,也可以作为 props 传入
<GrandChild>
。
禁用 Attributes 继承
在选项式API中,在组件选项中设置 inheritAttrs: false
可以禁止 组件自动地继承 attribute。
在组合式API中,在 <script setup>
中使用 defineOptions
:
html
<script setup>
defineOptions({
inheritAttrs: false
})
// ...setup 逻辑
</script>
通过设置 inheritAttrs
选项为 false
,可以完全控制透传进来的 attribute 被如何使用。
注意:透传进来的 attribute 可以在模板的表达式中直接用 $attrs
访问到。
在ChildComponent.vue
中,设置 inheritAttrs: false
并通过$attrs
访问透传属性 customValue
:
html
<!-- ChildComponent.vue 的模板 -->
<template>
<div>在ChildComponent.vue中,读取透传 Attributes: {{ $attrs.customValue }}</div>
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false
})
</script>
在父组件中,使用子组件ChildComponent.vue
,传递class
属性、customValue
属性:
html
<template>
<div class="home-wrap">
<h1>父组件</h1>
<ChildComponent class="child-div" :customValue="customValue" />
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const customValue = ref(10)
</script>
渲染出的DOM结果是:
html
<div>在ChildComponent.vue中,读取透传 Attributes: 10</div>
可以看到,父组件传的class
属性并没有被直接透传到子组件的根元素上。
通过Vue Devtools查看$attrs
,$attrs
包含了 透传进来的 class
、customValue
属性:
$attrs
对象包含了除组件所声明的 props
和 emits
之外的所有其他 attribute,例如 class
,style
,v-on
监听器等等。
-
和
props
有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像foo-b
ar 这样的一个 attribute 需要通过$attrs['foo-bar']
来访问。 -
像
@click
这样的一个v-on
事件监听器将在此对象下被暴露为一个函数$attrs.onClick
。
通过设定 inheritAttrs: false
和使用 v-bind="$attrs"
来实现将 透传 attribute 应用在合适的元素上:
html
<!-- ChildComponent.vue 的模板 -->
<template>
<div>
<div v-bind:="$attrs">在ChildComponent.vue中,读取透传 Attributes: {{ $attrs.customValue }}</div>
</div>
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false
})
</script>
渲染出的DOM结果是:
html
<div>
<div class="child-div" customvalue="10">
在ChildComponent.vue中,读取透传 Attributes: 10
</div>
</div>
通过不带参数的 v-bind
,将一个对象的所有属性都作为 attribute 应用到目标元素上。
多根节点的 Attributes 继承
和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。
修改ChildComponent.vue
:
html
<!-- ChildComponent.vue 的模板 -->
<template>
<h2>ChildComponent的标题</h2>
<div>ChildComponent的内容</div>
</template>
此时,ChildComponent.vue
是多根节点模板,由于 Vue 不知道要将 attribute 透传到哪里,所以会抛出一个警告:
Extraneous non-props attributes (class, customValue) were passed to component but could not be automatically inherited because component renders fragment or text root nodes.
当ChildComponent.vue
有多个根节点时,需要显式绑定 $attrs
:
html
<!-- ChildComponent.vue 的模板 -->
<template>
<h2 v-bind="$attrs">ChildComponent的标题</h2>
<div>ChildComponent的内容</div>
</template>
在 JavaScript 中访问透传 Attributes
在选项式API中,attrs
会作为 setup()
上下文对象的一个属性暴露:
html
<script>
export default {
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs,且 没有响应性
console.log(ctx.attrs)
}
}
</script>
在组合式API中,在 <script setup>
中使用 useAttrs()
API 来访问一个组件的所有透传 attribute:
html
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
有个疑问待解决:
官网说法:
attrs
对象总是反映为最新的透传 attribute,但它没有响应性,不能通过侦听器去监听它的变化。
我直接在模板中使用$attrs
,以及使用上述2种方式获取attrs
,修改透传 attribute 后,页面也更新了。
所以没get到,官方说的attrs
对象没有响应性是指哪方面。
知道为什么的童鞋可以在评论区讲一下或者是私信给我说一下,非常感谢!