Vue自定义事件与slot插槽

自定义事件

主要用于在子组件上定义的自定义事件,并在子组件触发,由父组件监听和处理。

事件名

自定义事件的事件名应当使用 kebab-case(短横线命名法) ,和组件和prop不同,事件名不会被作为一个JavaScript变量名或属性名,所以没必要使用 camelCase 或 PascalCase 命名。并且 v-on 事件监听器在 DOM 模板中会自动转换成全小写(因为HTML是大小写不敏感的)。所以 v-on:myEvent 将会变成 v-on:myevent –而导致 myEvent 无法被监听到。

自定义事件使用方法

  • 在父组件用 v-on:event-name="eventHandle" 监听自定义事件 event-name ,并用 eventHandle 方法处理自定义事件 event-name
  • 在子组件用 this.$emit('event-name') 触发自定义事件 event-name

例:

HTML

1
2
3
4
5
6
7
<div id="app">
<p>{{ total }}</p>
<!--子组件-->
<!--自定义事件名为 sub-add ,指定的事件处理函数为 superAdd-->
<button-counter @sub-add="superAdd"></button-counter>
<button-counter @sub-add="superAdd"></button-counter>
</div>

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Vue.component('button-counter', {
template: '<button @:click="addCounter">{{ counter }}</button>',
data () {
return {
counter: 0
}
},
methods {
addCounter () {
this.counter++;
//触发自定义事件
this.$emit('sub-add');
}
}
});

new Vue({
el: '#app',
data: {
total: 0;
},
methods: {
superAdd () {
this.total++;
}
}
});

载荷(payload)的使用

子组件触发自定义事件后,可利用载荷(payload)向父组件传递数据,使得父组件可以使用子组件的数据。

例:

HTML

1
2
3
4
5
6
<div id="app">
<p>sub componet: {{ message }}</p>
<!--子组件-->
<!--自定义事件名为 sub-send-message ,指定的事件处理函数为 getSubMessage-->
<button-message @sub-send-message="getSubMessage"></button-message>
</div>

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Vue.componet('button-message', {
template: '<div><input type="text" v-model="message"><button @click="handleSendMessage">Send</button></div>',
data () {
return {
message: ``
};
},
methods: {
handleSendMessage () {
//触发自定义事件 send-sub-message ,载荷 payload 为 {subMessage: this.message}
this.$emit('sub-send-message ,载荷payload为', {subMessage: this.message});
}
}
});

new Vue({
el: '#app',
data: {
message: ''
},
methods: {
//在父组件中使用载荷 payload ,本例中载荷 payload 为 {subMessage: this.message}
getSubMessage (payload) {
this.message = payload.subMessage;
}
}
});

使用自定义事件实现非父子组件间的通信

有时候,非父子关系的两个组件之间也需要通信。在简单的场景下,可以使用一个空的 Vue 实例作为事件总线。

  • 创建一个空的 Vue 实例 buffer
  • 在组件 A 中 利用 buffer 触发自定义事件 并利用载荷 payload 传递组件A的数据
  • 在组件 B 的 mounted 钩子函数中,利用 buffer.$on('自定义事件名', (data) => {...}) 监听并处理自定义事件, buffer.$on() 的第二个参数是一个回调函数,该回调函数的参数是自定义事件触发时的载荷 payload ,该回调函数即自定义事件的处理程序

例:

HTML

1
2
3
4
5
6
<div id="app">
<!--组件A-->
<component-a></component-a>
<!--组件B-->
<component-b></component-b>
</div>

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var buffer = new Vue(); //创建一个空的 Vue 实例 `buffer`

//注册组件 A ,组件 B
Vue.component('componentA', {
template: '<p>this message from componentB: {{ message }}</p>',
data () {
return {
message;
}
},
//在组件 A 的 mounted 钩子函数中监听并处理自定义事件
mounted () {
//通过 buffer 监听并处理自定义事件
buffer.$on('send-message', (data) => {
this.message = data;
});
}
});

Vue.component('componetB', {
template: '<div><input type="text" v-model="message"><button @click="sendMessage">Send</button></div>',
data () {
return {
message: ''
};
},
methods: {
sendMessage () {
//通过 buffer 触发自定义事件, 载荷 payload 为 this.message
buffer.$emit('send-message', this.message);
}
}
});

slot插槽

使用组件时,组件标签内也可以嵌套内容, slot 可以将组件标签内也可以嵌套的内容插入到组件内部的具体某个位置。如果组件内没有包含一个 <slot> 元素,则组件标签内嵌套的内容都会被抛弃

单个插槽 | 默认插槽 | 匿名插槽

即没有 name 属性的 <slot> 标签,一个组件中只能有一个该类插槽 ,如下例:

父组件

1
2
3
4
5
6
7
8
9
10
<template>
<div class="father">
<p>这是父组件</p>
<!--子组件-->
<child>
<!--子组件标签内的嵌套内容-->
<p>Hello World!</p>
</child>
</div>
</template>

子组件

1
2
3
4
5
6
7
<template>
<div class="child">
<p>这是子组件</p>
<!--将父组件中子组件标签内的嵌套内容放到这里-->
<slot></slot>
</div>
</template>

最终渲染结果

1
2
3
4
5
6
7
<div class="father">
<p>这是父组件</p>
<div class="child">
<p>这里是子组件</p>
<p>Hello World!</p>
</div>
</div>

上例可以看出,插槽的内容为父组件在子组件标签中放置的内容,插槽的在子组件中的位置由子组件中 <slot> 标签的位置决定。

具名插槽

插槽加了 name 属性,就变成了具名插槽。一个组件可以有多个 name 属性不同的具名插槽在插槽内容上设定 slot 属性来与 name 值相同的插槽对应所有没有 slot 属性的内容都对应到匿名插槽 。如下例:

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="father">
<p>这是父组件</p>
<!--子组件-->
<child>
<!--与 name 值为 "slotA" 的插槽对应-->
<p slot="slotA">具名插槽A</p>
<!--与 name 值为 "slotB" 的插槽对应-->
<p slot="slotB">具名插槽B</p>
<!--无 solt 属性,与匿名插槽对应-->
<p>匿名插槽1</p>
<p>匿名插槽2</p>
</child>
</div>
</template>

子组件

1
2
3
4
5
6
7
8
<template>
<div class="child">
<slot></slot>
<p>这是子组件</p>
<slot name="slotB"></slot>
<slot name="slotA"></slot>
</div>
</template>

渲染结果

1
2
3
4
5
6
7
8
9
10
<div class="father">
<p>这是父组件</p>
<div class="child">
<p>匿名插槽1</p>
<p>匿名插槽2</p>
<p>这是子组件</p>
<p>具名插槽B</p>
<p>具名插槽A</p>
</div>
</div>

注:所有没有 slot 属性的内容都对应到匿名插槽

默认插槽的内容

可以为 <slot> 标签提供默认内容,当父组件没有提供插槽内容时显示默认内容,如果父组件为这个插槽提供了内容,则默认的内容会被替换掉。

子组件

1
2
3
4
5
<div class="child">
<p>这是子组件</p>
<!--如果父组件为该插槽提供了默认内容,则默认的内容会被替换掉-->
<slot>插槽的默认内容</slot>
</div>

编译作用域

插槽内容是写在父组件的,所以它可以使用父组件的数据,而不能使用子组件的数据。父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。如下例:

父组件

1
2
3
4
5
6
<div class="father">
<p>这是父组件</p>
<child>
<p>Message: {{ message }}</p>
</child>
</div>

1
2
3
4
5
6
//父组件的 data 部分
data () {
return {
message: '来自父组件的message'
}
}

子组件

1
2
3
4
<div class="child">
<p>这是子组件</p>
<slot></slot>
</div>

1
2
3
4
5
6
//子组件的 data 部分
data () {
return {
message: '来自子组件的message'
};
}

渲染结果

1
2
3
4
5
6
7
8
<div class="father">
<p>这是父组件</p>
<div class="child">
<p>这是子组件</p>
<!--插槽在父组件编译,所以使用的 message 是父组件的 message ,不是子组件的 message ,也无法使用子组件的数据-->
<p>来自父组件的message</p>
</div>
</div>

作用域插槽

子组件可以将自己的数据发给父组件来处理,即在插槽内容中使用子组件的数据。

  • 先在父组件的插槽内容上用 slot-scope 属性指定一个对象用来存放子组件的数据,例:slot-scope="childData"注意 childData 对象只在指定了 slot-scope 属性的标签能用,该标签的父标签和子标签都不能用
  • 在子组件的 <slot> 标签上用 v-bind 指令来指定要传给父组件的插槽内容的键值对,
    例: :childMessage="message"childMessage会成为 slot-scope 指定的对象的属性,其属性值为子组件的 message 属性。

父组件

1
2
3
4
5
6
7
8
<div class="father">
<p>这是父组件</p>
<child>
<!--子组件的传上来的数据作为 childData 对象的属性-->
<!--`childData` 对象只在指定了 `slot-scope` 属性的标签能用,该标签的父标签和子标签都不能用-->
<p slot-scope="childData">Message: {{ childData.childMessage }}</p>
</child>
</div>

1
2
3
4
5
6
//父组件的data部分
data () {
return {
message: '来自父组件的message'
};
}

子组件

1
2
3
4
5
<div class="child">
<P>这是子组件</P>
<!--子组件传递 message 属性给父组件,该属性值保存在 childData 对象的 childMessage 属性中-->
<slot :childMessage="message"></slot>
</div>

1
2
3
4
5
6
//子组件的 data 部分
data () {
return {
message: '来自子组件的message'
};
}

渲染结果

1
2
3
4
5
6
7
<div class="father">
<p>这是父组件</p>
<div class="child">
<p>这是子组件</p>
<p>来自子组件的message</p>
</div>
</div>