Skip to content

组件通信

1.ref

ref的作用

  • 可以获取一个真实dom,访问元素身上的属性或方法
  • 可以获取一个组件实例,访问组件data中的属性,methods中的方法

使用方式:

1.给标签或组件添加ref属性,值为一个字符串

html
<h1 ref="title"></h1>

<Child ref="child" />

2.通过this.$refs.字符串使用

js
export default {
    components:{ Child },
    methods: {
        showDOM(){
            console.log(this.$refs.title)	// 真实DOM元素
            console.log(this.$refs.child)		// 真实DOM元素
        }
    },
}

2.props

props是一个配置项,用于接受父组件穿过来的属性

基本语法如下

vue
<template>
	<Child :num="num"/>
</template>

<script>
export default{
    components:{
        Child:()=>import('./Child')
    },
    data(){
        return{
            num:666
        }
    }
}
</script>
vue
<template>
	{{num}}
</template>

<script>
export default{
    props:['num']
}
</script>

子组件接收数据有三种方式:

  1. 数组,只用写数据名,不可限制类型

    js
    props:['name', 'age']
  2. 对象,可限制类型

    js
    props:{
        name:String,
        age:Number
    }
  3. 对象,可限制类型、必要性、默认值

    js
    props: {
        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()可调用父子间中的这个事件

基本语法如下

  1. 在父组件中给子组件添加自定义事件,定义时只用写方法名,例如事件名为emitEventName,方法名为add
  2. 在子组件中使用this.$emit()调用父组件中的自定义事件
    1. 第一个参数为事件名emitEventName,后面可以传若干个参数,都是函数的实参
vue
<template>
	<Children  @emitEventName="add" />
</template>

<script>
    export default{
        methods: {
            add(value1,value2){
                return value1+value2
            }
        }
    }
</script>
vue
<template>
	<button @click="sendValue">
        点击发送数据到父组件
    </button>
</template>

<script>
    export default{
        methods: {
            sendValue(){
                this.$emit('add',111,222)
            }
        }
    }
</script>

也可以用如下方法,不过比较麻烦不推荐

在父组件中this.$refs.demo.$on('事件名',方法)

js
<Demo ref="demo"/>
//......
methods(){
    getValue(){
        //...    
    }
},
mounted(){
   this.$refs.demo.$on('add',this.getValue)
},
beforeDestroy(){
    this.$refs.demo.$off('add')        //组件销毁前,需要解绑自定义事件
}
  1. 若想让自定义事件只能触发一次,可以使用once修饰符,或把$on改成$once

  2. 触发自定义事件this.$emit('事件名',数据)

  3. 解绑自定义事件this.$off('事件名')

  4. 组件绑定原生事件不生效怎么办?

    • 绑定原生DOM事件,需要使用native修饰符 @click.native="show"
    • 上面绑定自定义事件,即使绑定的是原生事件也会被认为是自定义的,需要加native,加了后就将此事件给组件的根元素
  5. **注意:**通过this.$refs.xxx.$on('事件名',回调函数)绑定自定义事件时,回调函数要么配置在methods中,要么用箭头函数,否则 this 指向会出问题

3.2 观察者模式

自定义事件原理使用了发布-订阅模式(EventEmitter)或者观察者模式,通过事件的注册、触发和监听来处理事件和进行组件间的通信,大致代码如下

js
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()方法的简要源码

js
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()方法的大致源码

js
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的自定义事件

vue
<template>
  <div>
    <Child @myEvent="handleEvent"></Child>
  </div>
</template>

然后在子组件中打印this,查看_events属性

image-20241122103402664

可以看出,_events是一个对象,里面存放着我们在父组件中定义的自定义事件,而且每一个事件都是一个数组,这很符合观察者模式

由此可以得出:父组件给子组件添加自定义事件时,内部是给子组件身上的_events添加了一个事件,并且事件里的回调函数指向父组件里定义的函数,所以使用$emit()调用时,会调用父组件中的方法

4.sync修饰符

在有些情况下,我们可能需要对一个 prop 进行“双向绑定”,但由于单向数据流,子组件无法直接修改父组件的数据

通常我们通过$emit来实现父子组件双向绑定,如下

  • 父组件调用子组件,并把自身的val属性传递给子组件
  • 子组件通过$emit触发父组件update:属性名方法,更新父组件val属性
vue
<Child :val="val" @update:val="(v) => (val = v)" />
<script>
    export default {
        data() {
            return {
                val: 0,
            };
        },
    };
</script>
vue
<div>
    <input type="text" :value="val" />
    <button @click="$emit('update:val', val+1)">+</button>
</div>

通过.sync修饰符,我们可以简化以上操作

vue
<Child :val.sync="val" />
vue
<div>
    <input type="text" :value="val" />
    <button @click="$emit('update:val', val+1)">+</button>
</div>

可以看到,两者的区别在于父组件在使用.sync修饰符之后,省略了自定义事件

7.全局事件总线

一种可以在任意组件间通信的方式,本质上就是一个对象,它必须满足以下条件

  1. 所有的组件对象都必须能看见他

  2. 这个对象必须能够使用 $on $emit $off 方法去绑定、触发和解绑事件

7.1 使用步骤

1.定义全局事件总线(在main.js文件中定义)

js
new Vue({
   	beforeCreate() {
   		Vue.prototype.$bus = this // 安装全局事件总线,$bus 就是当前应用的 vm
   	},
})

2.使用事件总线

监听事件:A组件想接收数据,则在A组件中给 $bus 绑定自定义事件,事件的回调留在A组件自身

  • $bus.$on('xxx',()=>{}) 接收数据
  • $bus.$off('xxx') 解绑自定义事件
js
export default {
    mounted() {
        this.$bus.$on('xxx',(data)=>{   //利用$on绑定事件,xxx为自定义总线名
           	console.log(data)
        })  
    },
    beforeDestroy() {                    //必须在组件销毁之前给总线用$off解绑
         this.$bus.$off('xxx')        
    }
}

触发事件this.$bus.$emit('xxx',data)

js
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调用自身的这个事件

vue
<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

js
new Vue({
   	beforeCreate() {
   		Vue.prototype.$bus = this // 安装全局事件总线,$bus 就是当前应用的 vm
   	},
})

或者如下写法也可以

js
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组件自身

js
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

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

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. 分类:默认插槽、具名插槽、作用域插槽

1.默认插槽

子组件Category中:

html
<div>
	<!-- 定义插槽 -->
	<slot>插槽默认内容...</slot>
</div>

父组件中:

默认显示在子组件的slot的位置

html
<Category>
    <div>html结构1</div>
</Category>

2.具名插槽

  1. 父组件指明放入子组件的哪个插槽slot="slotName"

  2. 如果元素过多可以用<template></template>标签包裹,不会生成实际的标签元素,并且指定插槽名可以写成v-slotName,或者#slotName

  3. v-slot:footerslot="footer"作用一样,但只能写在template标签内

子组件Category中定义插槽:

html
<template>
    <div>
        <!-- 定义插槽 -->
        <slot name="center">插槽默认内容...</slot>
        <slot name="footer">插槽默认内容...</slot>
    </div>
</template>

父组件中使用插槽:

html
<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中:

html
<template>
    <div>
        <!-- 把自身模型数据games传递出去,val为自定义 -->
        <slot :val="games"></slot>		
    </div>
</template>

<script>
    export default {
        name:'Category',
        //数据在子组件自身
        data() {
            return {
                games:['红色警戒','穿越火线','劲舞团','超级玛丽']
            }
        },
    }
</script>

父组件中:

  • 使用scope属性接收传过来的数据,注意不是scoped
  • 传来的是个对象,包含所有传递过来的数据
html
<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 作为真正的来源。

MIT Licensed