Vue组件通信
Vue组件的通信关系:父子关系,兄弟关系、祖先后代关系、无直接(非简单)关系。
父子关系
父子关系的双工通信方式,主要有以下几种方法:
- 父向子传递关系:props、$attrs
- 父向子访问关系:$children、ref
- 子向父亲传递关系:$emit
- 子向父亲访问关系:$parent、$listeners
Vue提倡一种理念:祖先(层级高的)组件向子孙(层级低的)组件传递的值,是不能直接修改的。下面通过代码详细说明父子关系组件通信的几种情况。
假定APP是最高级根组件,存在A组件和E组件作为App组件的儿子。
App.vue
<template>
<div style="border:1px solid #343443;padding:20px;">
<div style="display:inline-block;">
<A :msg="msg" ref="A" @toAlert="Alert" />
<button @click="onClick" style="margin:10px;">
点击后,修改A组件data中refValue值
</button>
</div>
<div style="display:inline-block;margin-left:10px;">
<E :msg1="msg1" @eval="getE($event)"/>
<p>等待子组件抛出值:{{childVal}}</p>
<button @click="onClick1" style="margin:10px;">
点击后,修改E组件data中childValue值
</button>
</div>
</div>
</template>
<script>
import A from "./components/A";
import E from "./components/E";
export default {
name: "app",
data() {
return {
msg: `子组件props方式:${this.$options.name}(父)向A组件(子)传递属性msg`,
msg1: `子组件$attrs方式:${this.$options.name}(父)向E组件(E)传递属性msg1`,
parentVal:'app中的值',
childVal:"用于保留子组件抛出的值"
};
},
components: {
A,
E
},
methods: {
Alert() {
alert("来自APP组件的弹框");
},
onClick() {
this.$refs["A"].refValue =
`${this.$options.name}修改了A组件的refValue值`;
},
onClick1() {
for (let i in this.$children) {
if (this.$children[i].$options.name === "E") {
this.$children[i].childValue =
`${this.$options.name}修改了E组件的childValue`;
}
}
},
getE(e){
this.childVal = e;
},
};
</script>
在APP组件(父)中向A组件(子)和E组件(子)分别绑定msg和msg1,APP中定义方法onClick方法,点击按钮通过ref引用组件A的实例,从而访问A组件的数据和方法,onClick1方法通过$children的方式遍历查找目标孩子组件实例,这里查找了E组件,从而访问E组件的数据和方法,这里可以封装成更通用的方法。childVal保存子组件抛出的值,当子组件emit后,触发getE。
A.vue
<template>
<div style="border:1px solid #343443;padding:20px;">
<h1>A组件:B组件是我儿子</h1>
<p>{{msg}}</p>
<p>{{refValue}}</p>
<button style="margin:10px;" @click="$listeners.toAlert">
A组件:调用app组件(根)的方法
</button>
</div>
</template>
<script>
export default {
name:"A",
props:{
msg:String
},
data(){
return {
refValue:`${this.$options.name}组件data中refValue值`,
}
},
}
</script>
A组件(子)通过props对象保留App(父)组件传递的值,refValue供App组件通过ref方式访问,button按钮点击后,通过$listeners的方式访问App组件中的方法。
E.vue
<template>
<div style="border:1px solid #787878;padding:10px;">
<h1>E组件:A组件是我兄弟</h1>
<p>{{$attrs.msg1}}</p>
<p>{{childValue}}</p>
<p>访问父组件值{{$parent.parentVal}}</p>
<button @click="onClick">向父亲提供时机,抛出值<button>
<F/>
</div>
</template>
<script>
export default {
name:"E",
data(){
return{
childValue:`${this.$options.name}组件中data的childValue值`,
emitVal:'E组件的emit出来的值'
}
},
methods:{
onClick(){
this.$emit('eval',emitVal)
}
}
}
</script>
E组件(子)通过$attrs接收APP组件(父)传递的属性,childValue供App中onClick2方法访问,在p标签中通过$parent的方式访问APP的值,点击按钮,$emit自定义事件向App组件抛出一个值。
根据官方文档中的说明,指出 $parent、$children应当适当使用!
其中对于子组件访问父组件所用的$parent和$listeners,这种方式可以向下传递,也就是继续把这两个值向A或者E的子组件传递,后续会引出A组件的儿子B组件和A组件的孙子D组件,这个故事还没结束。
兄弟关系
按照上面例子来看,A组件和E组件就是兄弟关系,兄弟关系使用搭桥的方式,也就是通过他们共同的父亲来传值。这里不谈总线模式,总线模式可以说是适用于任何组件关系的传值。
这里忽略上述已经表述过的代码!
A.vue
<script>
export default {
...
data(){
return {
...
brother:"A组件的值"
...
}
}
methods:{
...
onClick(){
this.$parent.$emit('aclick',this.brother);
}
...
},
...
}
</script>
通过按钮来触发这个onClick事件,A和E共同的父亲就是this.$parent,这就是搭桥的思量,也就是A和E组件的父亲等待接受一个值,这个感知的过程在父亲组件,但实际的传值和取值本身还是在各自组件内。
E.vue
<script>
export default{
...
data(){
return {
...
brotherVal:"用于接受A组件(兄)的值"
...
}
},
mounted(){
...
this.$parent.$on('aclick',(e)=>{
this.brotherVal = e;
})
...
}
...
}
</script>
这里只演示了A向E的传值过程,按照同样的道理,反之,E也可以向A进行传值。
祖先后代关系
- 祖先向子孙传值: $listeners、provide & inject
- 子孙向祖先传值: 暂时没有想到什么好方法、没有办法就用总线、哈哈
那这里就主要展示$listeners和provide&inject如何使用,简单描述下组件结构,上述提到App有儿子A和E,现在假设A组件有儿子B,B组件有儿子D组件,下面的代码会有所省略。
$listeners 演示代码 App.vue
<template>
...
<A :msg="msg" ref="A" @toAlert="Alert" />
...
</template>
<script>
export default{
...
methods:{
Alert() {
alert("来自APP组件的方法");
},
}
...
}
</script>
A.vue
<template>
...
<B v-on="$listeners"/>
...
</template>
B.vue
<template>
...
<D v-on="$listeners"/>
...
</template>
D.vue
<template>
...
<button @click="$listeners.toAlert">
D组件:调用app组件(根)的方法
</button>
...
</template>
实际上这就是将$listeners对象不断传递给子组件的过程。
provide&inject 演示代码
App.vue
<template>
<button @click="onClick2">
点击按钮,App组件向D组件(子孙或后代)provide传值
</button>
</template>
<script>
export default{
...
data() {
return {
val: "默认值",
val1: "",
val2: "默认值",
};
},
provide() {
this.val = Vue.observable({
Val: "provide中注册一个响应值"
});
this.val1 = Object.defineProperty({}, "Val", {
value: "初始值",
writable: true,
configurable: true,
enumerable: false
});
this.data = new Proxy(this.$data, {
get: (target, name) => {
return name in target ? target[name] : "undefined";
}
});
return {
val: this.val,
val1: this.val1,
data: this.data
};
},
methods:{
...
onClick2() {
alert("修改provide中的val值");
this.val.Val = "默认值=>修改provide中的val值";
this.val1.Val = "修改provide的val1值";
this.data.val2 = "修改provide的val2值";
}
...
}
...
}
</script>
App组件中在provide提供了2中注册响应值的方式,vue.observable在vue2中是对defineProperty的封装,在vue3中是对Proxy的封装,这样注册的变量在点击按钮后,修改后的新的值可以同步到子孙组件中。
D.vue
<template>
...
<h3>D组件:我的父亲是B组件</h3>
<p>准备接受根组件inject值:{{val.Val}}</p>
<p>通过defineproperty,inject传递的值:{{val1.Val}}</p>
<p>通过proxy,inject传递的值:{{data.val2}}</p>
...
</template>
<script>
export default{
...
inject:['val','val1','data'],
...
}
</script>
inject接受祖辈组件传过来的值,语法上和props类型,存在数组和对象的方式,具体的差异可以参考官方文档。
非简单关系的组件
因为非简单关系,想想脑壳就疼,所以提供了傻瓜式的总线模式,但为了更好的管理总线上的事件,如果组件关系并非很复杂,请使用上述介绍的方案,本文并不会对组件通信的优缺点进行说明,萝卜白菜各有所爱,但各自方法需要注意的点,参见vue官方文档API。
现在已有E组件、A组件的子孙D组件,假设E组件存在F组件(子)。那么F组件和D组件的传值就是一种非简单关系。以下代码存在省略。
D.vue
<template>
...
<p>准备接受F组件(复杂关系)的值:{{localVal}}</p>
...
</template>
<script>
...
data(){
return {
localVal:""
}
},
mounted() {
this.$root.$on('ftod',(e)=>{
this.localVal = e;
})
},
</script>
$root就可以理解为根组件实例,这里$root就是所谓的总线,$on监听事件,等待$emit触发。
F.vue
<template>
<div style="border:1px solid #787878;">
<h2>F组件:C、D组件与我的关系难以描述</h2>
<button @click="onClick" style="margin:10px;">
点击按钮,向D组件传值
</button>
</div>
</template>
<script>
export default {
name:"F",
data(){
return {
mi:'F组件data中的值'
}
},
methods:{
onClick(){
this.$root.$emit('ftod',this.mi)
}
}
}
</script>
通过点击事件发送emit事件,传递mi值,这是最简单的总线使用方式。 Vue本身已经实现了$on and $emit 方法,另外还存在这样一种实现总线的方式。
export new Vue()
下面通过代码演示基本原理
$on、$emit基本原理
Bus.js
class Bus{
constructor(){
this.callbacks = {}
}
$on(name,fn){
this.callbacks[name] = this.callbacks[name] || []
this.callbacks[name].push(fn)
}
$emit(name,args){
if(this.callbacks[name]){
this.callbacks[name].forEach(cd=>cb(args))
}
}
}
export new Bus()
为了方便调用,挂在到Vue实例上!
main.js
import Vue from 'vue'
import bus from './Bus'
Vue.prototype.$bus = bus;
##总结 以上说明了绝大多数组件通信的情况,算是Vue知识的普及,提供给大家补充基础知识。为了方便实践,这里提供一份demo,如果在练习过程中存在代码问题,欢迎交流。微信:cch_lover 唯一的联系方式。