组件通信
1.ref
ref的作用
- 可以获取一个真实dom,访问元素身上的属性或方法
- 可以获取一个组件实例,访问组件data中的属性,methods中的方法
使用方式:
1.给标签或组件添加ref属性,值为一个字符串
<h1 ref="title"></h1>
<Child ref="child" />2.通过this.$refs.字符串使用
export default {
components:{ Child },
methods: {
showDOM(){
console.log(this.$refs.title) // 真实DOM元素
console.log(this.$refs.child) // 真实DOM元素
}
},
}2.props
props是一个配置项,用于接受父组件穿过来的属性
基本语法如下
<template>
<Child :num="num"/>
</template>
<script>
export default{
components:{
Child:()=>import('./Child')
},
data(){
return{
num:666
}
}
}
</script><template>
{{num}}
</template>
<script>
export default{
props:['num']
}
</script>子组件接收数据有三种方式:
数组,只用写数据名,不可限制类型
jsprops:['name', 'age']对象,可限制类型
jsprops:{ name:String, age:Number }对象,可限制类型、必要性、默认值
jsprops: { name: { type: String, // 类型 required: true,// 必要性 default: '张三',// 默认值,没有传值默认显示张三 validator:function(){ } }, age:{ type: Number, // 类型 required: true,// 必要性 default: '18'// 默认值 } }
WARNING
备注:props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中,然后去修改data中的数据
3.自定义事件
3.1 基本使用
自定义事件事件是通过@或者v-on指令,给组件添加一个自定义事件,子组件通过$emit()可调用父子间中的这个事件
基本语法如下
- 在父组件中给子组件添加自定义事件,定义时只用写方法名,例如事件名为emitEventName,方法名为add
- 在子组件中使用
this.$emit()调用父组件中的自定义事件- 第一个参数为事件名emitEventName,后面可以传若干个参数,都是函数的实参
<template>
<Children @emitEventName="add" />
</template>
<script>
export default{
methods: {
add(value1,value2){
return value1+value2
}
}
}
</script><template>
<button @click="sendValue">
点击发送数据到父组件
</button>
</template>
<script>
export default{
methods: {
sendValue(){
this.$emit('add',111,222)
}
}
}
</script>也可以用如下方法,不过比较麻烦不推荐
在父组件中this.$refs.demo.$on('事件名',方法)
<Demo ref="demo"/>
//......
methods(){
getValue(){
//...
}
},
mounted(){
this.$refs.demo.$on('add',this.getValue)
},
beforeDestroy(){
this.$refs.demo.$off('add') //组件销毁前,需要解绑自定义事件
}若想让自定义事件只能触发一次,可以使用
once修饰符,或把$on改成$once触发自定义事件
this.$emit('事件名',数据)解绑自定义事件
this.$off('事件名')组件绑定原生事件不生效怎么办?
- 绑定原生DOM事件,需要使用native修饰符
@click.native="show" - 上面绑定自定义事件,即使绑定的是原生事件也会被认为是自定义的,需要加native,加了后就将此事件给组件的根元素
- 绑定原生DOM事件,需要使用native修饰符
**注意:**通过
this.$refs.xxx.$on('事件名',回调函数)绑定自定义事件时,回调函数要么配置在methods中,要么用箭头函数,否则 this 指向会出问题
3.2 观察者模式
自定义事件原理使用了发布-订阅模式(EventEmitter)或者观察者模式,通过事件的注册、触发和监听来处理事件和进行组件间的通信,大致代码如下
class EventEmitter {
constructor() {
this._events = {};//用对象的方式来缓存订阅者队列(事件名称:回调)
}
on(eventName, listener) {
if(typeof listener !== 'function') { return; }
if(!this._events) {//如果只被继承了prototype,需要在继承的对象上添加_events属性
this._events = Object.create(null);
}
if(!this._events[eventName]) {//事件队列不存在
this._events[eventName] = [];
}
this._events[eventName].push(listener);//添加观察者
}
addListener(eventName, listener) {
this.on(eventName, listener);
}
removeListener(eventName, listener) {
if(!this._events[eventName]) { return; }
this._events[eventName] = this._events[eventName].forEach(item => {
return item !== listener;
});
}
emit(eventName, ...args) {//状态改变
if(!this._events[eventName]) { return; }
this._events[eventName].forEach(callback => {//通知所有的订阅者,发起回调
callback.apply(this, ...args);
});
}
}vue中每个组件都有一个EventEmitter事件管理器
3.3 关键源码
接下来看一看一些源码
以下是$emit()方法的简要源码
Vue.prototype.$emit = function (event) {
var vm = this;
var cbs = vm._events[event];
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
var args = toArray(arguments, 1);
var info = "event handler for \"".concat(event, "\"");
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm;
};
function invokeWithErrorHandling(handler, context, args, vm, info) {
var res;
try {
res = args ? handler.apply(context, args) : handler.call(context);
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
res._handled = true;
}
}
catch (e) {
handleError(e, vm, info);
}
return res;
}可以看到回调是在vm实例上的_events对象中取出,然后交给invokeWithErrorHandling()函数把回调执行
再看一下$on()方法的大致源码
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
}
else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm;
};可以看到,$on()方法就是把注册的事件放在,vm._events里
所以可以得出,$on()和$emit()就是一个放,一个用,中间使用vm._events存储事件
当我们在一个父组件中给一个子组件添加了一个名为myEvent的自定义事件
<template>
<div>
<Child @myEvent="handleEvent"></Child>
</div>
</template>然后在子组件中打印this,查看_events属性

可以看出,_events是一个对象,里面存放着我们在父组件中定义的自定义事件,而且每一个事件都是一个数组,这很符合观察者模式
由此可以得出:父组件给子组件添加自定义事件时,内部是给子组件身上的_events添加了一个事件,并且事件里的回调函数指向父组件里定义的函数,所以使用$emit()调用时,会调用父组件中的方法
4.sync修饰符
在有些情况下,我们可能需要对一个 prop 进行“双向绑定”,但由于单向数据流,子组件无法直接修改父组件的数据
通常我们通过$emit来实现父子组件双向绑定,如下
- 父组件调用子组件,并把自身的val属性传递给子组件
- 子组件通过
$emit触发父组件update:属性名方法,更新父组件val属性
<Child :val="val" @update:val="(v) => (val = v)" />
<script>
export default {
data() {
return {
val: 0,
};
},
};
</script><div>
<input type="text" :value="val" />
<button @click="$emit('update:val', val+1)">+</button>
</div>通过.sync修饰符,我们可以简化以上操作
<Child :val.sync="val" /><div>
<input type="text" :value="val" />
<button @click="$emit('update:val', val+1)">+</button>
</div>可以看到,两者的区别在于父组件在使用.sync修饰符之后,省略了自定义事件
7.全局事件总线
一种可以在任意组件间通信的方式,本质上就是一个对象,它必须满足以下条件
所有的组件对象都必须能看见他
这个对象必须能够使用 $on $emit $off 方法去绑定、触发和解绑事件
7.1 使用步骤
1.定义全局事件总线(在main.js文件中定义)
new Vue({
beforeCreate() {
Vue.prototype.$bus = this // 安装全局事件总线,$bus 就是当前应用的 vm
},
})2.使用事件总线
监听事件:A组件想接收数据,则在A组件中给 $bus 绑定自定义事件,事件的回调留在A组件自身
$bus.$on('xxx',()=>{})接收数据$bus.$off('xxx')解绑自定义事件
export default {
mounted() {
this.$bus.$on('xxx',(data)=>{ //利用$on绑定事件,xxx为自定义总线名
console.log(data)
})
},
beforeDestroy() { //必须在组件销毁之前给总线用$off解绑
this.$bus.$off('xxx')
}
}触发事件:this.$bus.$emit('xxx',data)
export default {
methods(){
sendMsg(data){
this.$bus.$emit('xxx',data) //xxx为总线事件名称,data为传输过去的数据
}
}
}TIP
通过$emit()调用绑定的方法,第二个参数以及后面的参数,都是实参
$on接受方法时,第二个参数以及后面的参数,都是形参
必须在组件销毁之前给总线用$off解绑
7.2 原理
当我们使用@或者v-on指令添加事件时,实际上是使用了vm.$on()方法注册事件
每个组件都有一个EventEmitter,实例身上都会有$emit,$on 方法
我们可以$on定义一个事件,并且使用$emit调用自身的这个事件
<template>
<button @click="$emit('myEvent', 123)">click</button>
</template>
<script>
export default {
created() {
this.$on('myEvent', data => {
console.log(data)
})
},
}
</script>我们发现$emit和自定义事件的$emit是一样的,不同的是我们使用$on定义的事件
当我们使用自定义事件时,也就是相当于使用$on给自己定义了一个事件,并且与一个子组件关联了起来
但是自定义事件只能用于父子通信,无法用于全局通信
解决的办法就是在vm的原型上添加一个Vue实例作为公共的工具
main.js
new Vue({
beforeCreate() {
Vue.prototype.$bus = this // 安装全局事件总线,$bus 就是当前应用的 vm
},
})或者如下写法也可以
Vue.protoType.$bus=new Vue()这样,所有组件的原型链中都有这个$bus,便可以实现全局数据共享
8.pubsub
消息订阅与发布(pubsub)消息订阅与发布是一种组件间通信的方式,适用于任意组件间通信
8.1 使用步骤
1.安装pubsub:npm i pubsub-js
2.引入:import pubsub from 'pubsub-js' (哪个组件有用到,就在哪个组件中引用)
3.接收数据:A组件想接收数据,则在A组件中订阅消息,订阅的回调留在A组件自身
export default {
methods: {
demo(msgName, data) {...}
}
...
mounted() {
this.pid = pubsub.subscribe('xxx',this.demo)
}
}4.提供数据:this.pid=pubsub.publish('xxx',data)
5.最好在beforeDestroy钩子中,使用 pubsub.unsubscribe(pid) 取消订阅(取消订阅的参数并非函数名称,而是订阅的id)
src/components/School.vue
<template>
<div class="school">
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
</div>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name: 'School',
data() {
return {
name:'尚硅谷',
address:'北京',
}
},
methods: {
demo(msgName, data) {
console.log('我是School组件,收到了数据:',msgName, data)
}
},
mounted() {
this.pubId = pubsub.subscribe('demo', this.demo) // 订阅消息
},
beforeDestroy() {
pubsub.unsubscribe(this.pubId) // 取消订阅
}
}
</script>
<style scoped>
.school{
background-color: skyblue;
padding: 5px;
}
</style>src/components/Student.vue
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<button @click="sendStudentName">把学生名给School组件</button>
</div>
</template>
<script>
import pubsub from 'pubsub-js'
export default {
name:'Student',
data() {
return {
name:'JOJO',
sex:'男',
}
},
methods: {
sendStudentName(){
pubsub.publish('demo', this.name) // 发布消息
}
}
}
</script>
<style scoped>
.student{
background-color: pink;
padding: 5px;
margin-top: 30px;
}
</style>9.slot 插槽
<slot>插槽:让父组件可以向子组件指定位置插入 html 结构,也是一种组件间通信的方式,
适用于 父组件 ===> 子组件
- 分类:默认插槽、具名插槽、作用域插槽
1.默认插槽
子组件Category中:
<div>
<!-- 定义插槽 -->
<slot>插槽默认内容...</slot>
</div>父组件中:
默认显示在子组件的slot的位置
<Category>
<div>html结构1</div>
</Category>2.具名插槽
父组件指明放入子组件的哪个插槽
slot="slotName"如果元素过多可以用
<template></template>标签包裹,不会生成实际的标签元素,并且指定插槽名可以写成v-slotName,或者#slotNamev-slot:footer与slot="footer"作用一样,但只能写在template标签内
子组件Category中定义插槽:
<template>
<div>
<!-- 定义插槽 -->
<slot name="center">插槽默认内容...</slot>
<slot name="footer">插槽默认内容...</slot>
</div>
</template>父组件中使用插槽:
<Category>
<div slot="center">html结构1</div> //插入到名称为center的插槽
<template v-slot:footer> //内容过多用template标签包裹
<div class="foot">
<a href="http://www.atguigu.com">经典</a>
<a href="http://www.atguigu.com">热门</a>
<a href="http://www.atguigu.com">推荐</a>
</div>
<h4>欢迎前来观影</h4>
</template>
</Category>3.作用域插槽
scope 用于父组件往子组件插槽放的 html 结构接收子组件的数据,子→父
理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定
(games数据在Category(子组件)组件中,但使用数据所遍历出来的结构由App组件决定)
子组件Category中:
<template>
<div>
<!-- 把自身模型数据games传递出去,val为自定义 -->
<slot :val="games"></slot>
</div>
</template>
<script>
export default {
name:'Category',
//数据在子组件自身
data() {
return {
games:['红色警戒','穿越火线','劲舞团','超级玛丽']
}
},
}
</script>父组件中:
- 使用
scope属性接收传过来的数据,注意不是scoped - 传来的是个对象,包含所有传递过来的数据
<Category>
<template scope="scopeData">
<ul> //games是子组件里的:games
<li v-for="g in scopeData.games" :key="g">{{g}}</li>
</ul>
</template>
</Category>
<!-- scope第二种写法 -->
<Category>
<template slot-scope="scopeData"> //scope另一种使用方式,两者没区别
<!-- 生成的是h4标题 -->
<h4 v-for="g in scopeData.games" :key="g">{{g}}</h4>
</template>
</Category>$parent
$parent可获取当前组件的父组件(如果有)
$root
可获取根组件
$children
可获取当前组件的所有子组件,类型为一个数组,包含所有子组件
$children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。
