我们可以把一个网页拆分成很多部分,每个部分就是我们代码中的一个组件,左侧整个区域代表方块1,拆分成3个灰色区域代表方块2,左下区域又分成更小的2个深色区域,分别用2个方块3表示,右侧拆分成更小的3个深色区域用3个方块3表示
所以,左侧的网页就可以用右侧的图来表示,一个复杂的网页,最终都可以拆分成小的组件。
右边的图,左上角的红线是表示父子组件传值,父组件通过props向子组件传值,子组件通过$emit触发向父组件传值。
中间的红线表示非父子传值(爷孙也是非父子),当然可以组件1通过props向子组件2传值,组件2通过props向子组件3传值。子组件3通过$emit触发向父组件2传值,子组件2通过$emit触发向父组件1传值。但是这种传值也很麻烦。
最下面这根红线表示非父子传值,当然你也可以通过和上面一样的方法一层一层的传值,但是代码将会变得无比复杂!
而官方对vue定义是轻量级的视图层框架,当出现了非常复杂的数据传递的时候,光靠着vue是解决不了的!
非父子组件传值一般2种方式:
官方提供的数据层框架vuex
利用发布订阅模式来解决(在vue中称为总线机制)
我们这里讲解第二种
直接来看代码例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>非父子组件间的传值(Bus/总线/发布订阅模式/观察者模式)
</title>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<child content="lcy"></child><!-- 两个child是兄弟组件,不是父子关系 -->
<child content="真的帅"></child>
</div>
<script>
Vue.prototype.bus = new Vue();
Vue.component('child', {
props: {
content: String
},
template: '<div @click="handleClick">{{content}}</div>',
methods: {
handleClick() {
this.bus.$emit('change', this.content);
}
},
mounted() {
this.bus.$on('change', function(msg){
alert(msg);
})
}
})
var vm = new Vue({
el: "#app",
data: {
},
})
</script>
</body>
</html>
运行结果
点击lcy之后弹出两次alert对话框"lcy"
点击真的帅之后弹出两次alert对话框"真的帅"
为什么是两次呢?
每个组件都是vue实例,我们在Vue原型中定义bus属性,这是一个vue实例,相当于全局总线,等同在ES6的class Vue中定义,只要以后new Vue实例或者创建组件的时候,每个组件上都会有bus这个属性,指向同一个Vue实例。
因为每个组件都会去挂载,挂载完之后会执行生命周期方法mounted方法,而在mounted方法里,我们的全局总线bus实例注册了对change事件的监听,所以每个组件都有对change事件的监听,$on监听当前实例bus上的自定义事件change。事件可以由vm.$emit
触发。触发后执行这里的回调函数,回调函数会接收所有传入事件触发函数的额外参数。
子组件child绑定了点击事件,点击后执行handleClick方法,方法this.bus.$emit('change', this,content)的执行会触发当前实例bus上监听的事件change,后面的附加参数this.content会传给监听器回调函数。而总线bus是每个组件都有的,所以触发了所有组件上监听的change事件,change事件的回调函数获取参数content,弹出alert框。这里如果点的"lcy",$emit附加的content就是"lcy",所以回调接收到的是"lcy",alert弹出2次"lcy"
现在的目标是希望点击其中一个组件的时候,另一个组件跟着改变自己的内容
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>非父子组件间的传值(Bus/总线/发布订阅模式/观察者模式)
</title>
<script src="https://unpkg.com/vue"></script>
</head>
<body>
<div id="app">
<child content="lcy"></child>
<child content="真的帅"></child>
</div>
<script>
Vue.prototype.bus = new Vue();
Vue.component('child', {
props: {
content: String
},
data() {
return {
myContent: this.content
}
},
template: '<div @click="handleClick">{{myContent}}</div>',
methods: {
handleClick() {
this.bus.$emit('change', this.myContent);
}
},
mounted() {
this.bus.$on('change', (msg) => {
this.myContent = msg;
})
}
})
var vm = new Vue({
el: "#app",
data: {
},
})
</script>
</body>
</html>
运行结果
这里为什么要在data里面加myContent : content呢?我直接改props里面的content不就可以实现效果了吗?
效果是可以实现,但是会报错,如下
每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。所以才会建立一个副本(不是引用相同地址)myContent去解决这个警告。
尽管运行正常,为什么要报这个警告呢?
试想,父组件content传的不是字符串,传的是自定义对象{name : "xxx"},现在在子组件直接修改这个对象this.content.name="aaa",结果就影响了父组件,如果父组件其他地方还引用这个对象就出现了意料之外的结果。
所以需要一个副本(不是指向同一个引用)myContent : content
注意:data{}中定义的对象不会相等!就是上面这个例子。定义数字和字符串因为复用常量池数据,会相等。
官方文档参考见这里单向数据流
关注、留言,我们一起学习。
===============Talk is cheap, show me the code================