多根节点组件
Vue.js 3.x 版本不再对组件的 template 选项进行“唯一根节点”的限制,而是允许 DOM 结构具备多个根节点,即下列结构的全局组件在 Vue.js 3.x 版本中是允许的。
app.component('my-box',{
template:`
<div class="first">第一个节点</div>
<div class="second">第二个节点</div>
<div class="third">第三个节点</button>
`
})
本节对这种结构可能出现的问题以及 Vue 框架的语法要求进行讲解。
1、 非 Prop 属性
在使用组件时,被子组件通过 props 选项接收的属性被称为“Prop 属性”,没有被子组件的 props 选项接收的属性被称为“非 Prop 属性”。Vue 框架规定:Prop 属性可以向子组件数据区那样进行调用,即使用 this 就可以访问到 Prop 属性;非 Prop 属性需要在子组件中使用 this.$attrs 来进行访问。
【示例 4-15】非 Prop 属性的访问。注册一个全局组件名为 my-phone,其中设置一个 Prop 属性名为brand,再设置一个非 Prop 属性名为 count。最终在组件的 template 选项中设置两个 div 容器访问这两个属性。
HTML 文档中对 my-phone 组件使用的代码如下所示。
<div id="app">
<my-phone brand="HuaWei" :count="150"></my-phone>
</div>
Vue 代码如下所示。
01 let app=Vue.createApp({})
02 app.component('my-phone',{
03 template:`
04 <div class="brand">品牌:{{this.brand}}</div>
05 <div class="count">数量:{{this.$attrs.count}}</div>
06 `,
07 props:['brand']
08 })
09 app.mount('#app')
上述代码中,<my-phone>在 HTML 代码中设置了两个参数:brand 和 count。在组件注册的代码中,第07 行将 brand 参数利用 props 选项进行接收,形成了 Prop 属性。而 count 参数没有用 props 选项进行接收,即是一个非 Prop 属性。
在组件的 template 模板中,第 04 行访问了 Prop 属性 brand,使用的是 this.brand 格式。第 05 行访问了非 Prop 属性 count,使用的是 this.$attrs.count 格式。
这里需要注意,非 Prop 属性也是单向数据流传递,即不能在组件注册代码中改变该属性的值,也不能将该变量通过 v-model 绑定在表单元素中。
同理,在使用组件时绑定的自定义事件,也可以通过 this$attrs 获取。与非 Prop 属性不同的是,绑定事件的名称,在 this.$attrs 中需要添加 on_前缀。
【示例 4-16】通过 this.$attrs 触发组件的事件。注册一个全局组件名为 my-alert,为该组件设置一个名为 message 的属性,在绑定一个名为 custom 的自定义事件。
HTML 文档中对 my-alert 组件使用的代码如下所示。
<div id="app">
<my-alert message="非 Prop 属性的使用" @custom="custom"></my-alert>
</div>
Vue 代码如下所示。
01 let app=Vue.createApp({
02 methods:{
03 custom(value){
04 window.alert(value)
05 }
06 }
07 })
08 app.component('my-alert',{
09 template:`
10 <button @click="btnClick">单击</button>
11 `,
12 methods:{
13 btnClick(){
14 this.$attrs.onCustom(this.$attrs.message);
15 }
16 }
17 })
18 app.mount('#app')
在上述代码中,HTML 文档使用<my-alert>组件时,设置了一个自定义属性 message,同时绑定了一个自定义事件 custom。由于在组件定义的 Vue 代码中,没有使用 props 来接收自定义属性 message,因此message 是一个非 Prop 属性。对于绑定的自定义事件,可以在组件中使用 this.$emit()进行调用,也可以使用 this.$attrs 进行访问。
从第 14 行中可以看出,自定义事件 custom 在 this.$attrs 对象中存储为 onCustom,这是读者需要注意的地方,触发该事件时,需要使用 this.$attrs.onCustom()来实现。
2、 组件事件的继承
Vue.js 3.0 框架规定:父组件的事件可以直接继承给具备该事件的子组件及其后代组件。
【示例 4-17】组件事件的继承。书写一个用于选择专业的组件 select-specialty。该组件中具备一个可供选择专业的下拉菜单。当用户选择某个专业的菜单项时,需要用弹窗在页面中显示选中的专业内容。
从本示例的要求来看,应该为组件中的下拉菜单绑定 input 或 change 事件,为了体现出组件事件的可传递性,我们将预计绑定到下拉菜单的 input 事件绑定在该组件的父组件中。测试该功能是否能够实现。
HTML 文档中对 select-specialty 组件使用的代码如下所示。
<div id="app">
<select-specialty @input="boxInput"></select-specialty>
</div>
Vue 代码如下所示。
01 let app=Vue.createApp({
02 methods:{
03 boxInput(){
04 window.alert(event.target.value);
05 }
06 }
07 })
08 app.component('select-specialty',{
09 template:`
10 <select v-model="specialty">
11 <option>前端工程师</option>
12 <option>PHP 工程师</option>
13 <option>Java 工程师</option>
14 <option>UI 设计师</option>
15 <option>项目架构师</option>
16 </select>
17 `,
18 data(){
19 return {
20 specialty:'UI 设计师'
21 }
22 }
23 })
24 app.mount('#app')
在上述代码中,input 事件绑定在了 HTML 文档中使用<select-specialty>组件的代码中,即该组件的父组件中。由于父组件中的事件子组件成员(即<select>标记对)也有,因此该事件会传递给子组件成员。
若不希望父组件的事件传递给子组件,可以在子组件的定义代码中使用 inheritAttrs 选项,该选项取值为 false 时,this.$attrs 对象依然可以收集父组件传递过来的属性和绑定的事件,但是父组件绑定的事件不会在继承给子组件成员,即子组件成员的相应事件触发后,也不会执行任何父组件事件的代码。
若要让组件 select-specialty 进制父子组件之间的事件传递,代码如下所示。
01 app.component('select-specialty',{
02 inheritAttrs: false,
03 template:`
04 <select v-model="specialty">
05 <option>前端工程师</option>
06 <option>PHP 工程师</option>
07 <option>Java 工程师</option>
08 <option>UI 设计师</option>
09 <option>项目架构师</option>
10 </select>
11 `,
12 data(){
13 return {
14 specialty:'UI 设计师'
15 }
16 }
17 })
在上述代码中,第 02 行为组件设置了 inheritAttrs 选项,并取值为 false,这样父组件绑定的 input 事件在第 04 行的<select>子组件成员中就不会起作用了。
3、 多根节点组件的规定
从 Vue.js 3.x 框架开始,组件的 template 选项可以接收多个根节点结构。在 Vue.js 2.x 框架中,对 template选项有必须是一个根节点的限制。
若组件具备多个根节点,同时父组件绑定了事件,根据默认情况下父组件的事件会继承给子组件的这一特性,Vue 框架会报出一个警告。警告内容如下所示。
[Vue warn]: Extraneous non-emits event listeners (click) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the "emits" option.
上述警告的含义是:外部的(即父组件)的非触发的事件监听器(单击事件)被传递给子组件,但是不能自动继承,因为组件将渲染为片段或文本根节点。如果侦听器只是组件自定义事件的侦听器,则需要使用“emit”选项来声明它。
上述警告的解决方案有以下两个:
禁止父组件向子组件继承事件:即在子组件中书写 inheritAttrs:false 选项。
在子组件中需要继承父组件事件的根节点上显式绑定$attrs,这被称为“隐式贯穿”行为。
【示例 4-18】多根节点组件的事件继承。书写一个名为 root-test 的组件,其中包含三个根节点。在父组 件中绑定单击事件,显式指定第二个根节点继承父组件的事件。
HTML 文档中对 select-specialty 组件使用的代码如下所示。
<div id="app">
<root-test @click="divClick"></root-test>
</div>
Vue 代码如下所示。
01 let app=Vue.createApp({
02 methods:{
03 divClick(){
04 window.alert(event.target.textContent);
05 }
06 }
07 })
08 app.component('root-test',{
09 // inheritAttrs: false,
10 template:`
11 <div class="first">第一个根节点</div>
12 <div class="second" v-bind="$attrs">第二个根节点</div>
13 <div class="third">第三个根节点</div>
14 `
15 })
16 app.mount('#app')
Vue 框架的官网指出:与单个根节点组件不同,具有多个根节点的组件不具有自动隐式贯穿行为(Auto fallthrough)。若未显式绑定$attrs,则会在运行时发出警告。
在上述代码中,第 10 行至第 14 行定义了组件中存在的三个根节点,同时父组件上绑定了 click 事件。 要想放置警告的发生,可以将第 09 行注释变为语句,禁止父组件将事件传递给子组件成员,这也是我们讲解的第一种警告解决方案。也可以像第 12 行那样,使用 v-bind 绑定$attrs,这样就指定了只有第二个根节点可以继承父组件的事件,同时也不会在运行时发出警告了。