众所周知,组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。那么组件之间如何通信呢?主要有,
props
、$emit/$on
、vuex
、$parent / $children
、$attrs/$listeners
和provide/inject
,本文将分别介绍组件间的通信方式。
image.png
上图表示了组件之间所有可能的关系
A 和 B、B 和 C、B 和 D 都是父子关系,C 和 D 是兄弟关系,A 和 C 是隔代关系(可能隔多代)
第一种 props/$emit
,适用于父子组件之间的通信
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vue组件间的通信</title>
</head>
<body>
<div id="app">
<h3>组件间的通信(父子组件之间的通信props和$emit)</h3>
<parent></parent>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('parent', {
template: `
<div>
<h2>父组件</h2>
<p>子组件传来的数据==>{{ fromChildMsg }}</p>
<child :msgParent="msgParent" @sendMsgToParent="childMsgHandle"></child>
</div>
`,
data(){
return {
msgParent:'父组件的数据',
fromChildMsg:''
}
},
methods: {
childMsgHandle(msg){
this.fromChildMsg = msg
}
}
})
Vue.component('child', {
template: `
<div>
<h3>子组件</h3>
<p>父组件传来的数据==>{{ msgParent }}</p>
</div>
`,
props:{
msgParent:{
type:String
}
},
data() {
return {
msgChild: '子组件的数据'
}
},
mounted () {
this.$emit('sendMsgToParent',this.msgChild)
}
})
const app = new Vue({
el: '#app',
data: {}
})
</script>
</html>
注意:在组件间通行过程中,Vue组件遵循单向数据流原则:就是数据只能通过 props 由父组件流向子组件,而子组件并不能通过修改 props 传过来的数据修改父组件的相应状态。原因是:所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
运行结果
image.png
第二种,$attrs
和 $listeners
,适用于后代组件的通信
-
$attrs
:包含了父作用域中不被prop
所识别 (且获取) 的特性绑定 (class
和style
除外)。当一个组件没有声明任何prop
时,这里会包含所有父作用域的绑定属性 (class
和style
除外),并且可以通过v-bind="$attrs"
传入内部组件。 -
$listeners
:包含了父作用域中的 (不含.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vue组件间的通信</title>
</head>
<body>
<div id="app">
<h3>组件间的通信(父组件和后代之间的通信$attrs和$listeners)</h3>
<parent />
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('parent', {
template: `
<div>
<h2>父组件</h2>
<p>子组件传来的数据==>{{ fromChildMsg }}</p>
<p>后代件传来的数据==>{{ fromProgenyMsg }}</p>
<child :msgParentToProgeny="msgParentToProgeny" :msgParent="msgParent" @sendMsgToParent="childMsgHandle" @progenyToParent="progenyMsgHandle"/>
</div>
`,
data(){
return {
msgParent:'父组件的数据',
msgParentToProgeny:'父组件数据给后代组件',
fromChildMsg:'',
fromProgenyMsg:''
}
},
methods: {
childMsgHandle(msg){
this.fromChildMsg = msg
},
progenyMsgHandle(msg){
this.fromProgenyMsg = msg
}
}
})
Vue.component('child', {
template: `
<div>
<h3>子组件</h3>
<p>父组件传来的数据==>{{ msgParent }}</p>
// 后代组件中能直接触发 progenyToParent 的原因在于:子组件调用 后代组件时,使用 v-on 绑定了 $listeners 属性
// 通过v-bind 绑定 $attrs 属性,后代组件可以直接获取到 父组件组件中传递下来的 props(除了 子组件中 props声明的)
<progeny v-bind="$attrs" v-on="$listeners" />
</div>
`,
props:{
msgParent:{
type:String
}
},
data() {
return {
msgChild: '子组件的数据'
}
},
mounted () {
this.$emit('sendMsgToParent',this.msgChild)
}
})
Vue.component('progeny',{
template:`
<div>
<h3>后代组件</h3>
</div>
`,
data(){
return {
progenyMsg:'后代组件的信息'
}
},
mounted () {
this.$emit('progenyToParent',this.progenyMsg)
}
})
const app = new Vue({
el: '#app',
data: {}
})
</script>
</html>
运行结果
image.png
第三种:事件总线方式,在项目规模不大的情况下,完全可以使用中央事件总线 的方式,中央事件总线 EventBus 非常简单,就是任意组件和组件之间打交道,没有多余的业务逻辑,只需要在状态变化组件触发一个事件,然后在处理逻辑组件监听该事件就可以。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vue组件间的通信</title>
</head>
<body>
<div id="app"></div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('child1', {
template: `
<div>
<h3>子组件1</h3>
<button @click="$EventBus.$emit('child1Event',child1Msg)">bus</button>
</div>
`,
data() {
return {
child1Msg:'组件1的信息'
}
}
})
Vue.component('child2', {
template: `
<div>
<h3>子组件2</h3>
</div>
`,
mounted () {
this.$EventBus.$on('child1Event',msg=>{
console.log(msg)
})
}
})
// 定义事件总线
const EventBus = new Vue()
Vue.prototype.$EventBus = EventBus
const app = new Vue({
el: '#app',
template: `
<div>
<h3>组件间的通信(父子组件之间的通信-事件总线)</h3>
<child1 />
<child2 />
</div>
`
})
</script>
</html>
image.png
第四种,provide
和 inject
,适用于根组件和后代组件通信
在父组件中通过 provider 来提供属性,然后在子组件中通过 inject 来注入变量。不论子组件有多深,只要调用了 inject 那么就可以注入在 provider 中提供的数据,而不是局限于只能从当前父组件的 prop 属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vue组件间的通信</title>
</head>
<body>
<div id="app">
<c1 />
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('c1', {
template: `
<div>
<h3>组件1</h3>
<p>组件1-{{ rootMsg }}</p>
<c11 />
</div>
`,
provide: {
c1Msg: '组件1信息'
},
inject: ['rootMsg']
})
Vue.component('c11', {
template: `
<div>
<h3>组件1-1</h3>
<p>组件1-1-{{ c1Msg }}</p>
<c111 />
</div>
`,
inject: ['c1Msg']
})
Vue.component('c111', {
template: `
<div>
<h3>组件1-1-1</h3>
<p>组件1-1-1{{ c1Msg }}</p>
</div>
`,
inject: ['c1Msg']
})
const app = new Vue({
el: '#app',
provide: {
rootMsg: '根信息'
}
})
</script>
</html>
image.png
第五种 v-model,适用于父子组件的通信
v-model
用于父子组件之间的通信,体现的是v-model
的实现原理:绑定value
,监听input
事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vue组件间的通信</title>
</head>
<body>
<div id="app">
<parent />
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('parent',{
template:`
<div>
<h3>父组件</h3>
<p>父组件==>{{ message }}</p>
<child v-model="message" />
</div>
`,
data(){
return {
message:''
}
}
})
Vue.component('child',{
template:`
<div>
<input type="text" v-model="msg" @input="changeValueHandle" />
</div>
`,
props:{
value:{
type:String //v-model 会自动传递一个字段为 value 的 props 属性
}
},
data(){
return {
msg:''
}
},
methods:{
changeValueHandle(){
this.$emit('input',this.msg)
}
}
})
const app = new Vue({
el: '#app'
})
</script>
</html>
image.png
第六种 $parent
和$children
,适用于父子组件之间的通信
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vue组件间的通信</title>
</head>
<body>
<div id="app">
<parent />
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
Vue.component('parent', {
template: `
<div>
<h2>父组件</h2>
{{ childMsgFromParent }}
<button @click="getChildMsgHandle">获取子组件信息</button>
<child />
</div>
`,
data() {
return {
parentMsg: '父组件的信息',
childMsgFromParent:''
}
},
methods: {
getChildMsgHandle() {
this.childMsgFromParent = this.$children[0].childMsg
console.log(this.$children[0].childMsg)
}
}
})
Vue.component('child', {
template: `
<div>
<h3>子组件</h3>
{{ parentMsgFromChild }}
<button @click="getParentMsgHandle">获取父组件信息</button>
</div>
`,
data() {
return {
childMsg: '子组件信息',
parentMsgFromChild:''
}
},
methods: {
getParentMsgHandle() {
this.parentMsgFromChild = this.$parent.parentMsg
console.log(this.$parent.parentMsg)
}
}
})
const app = new Vue({
el: '#app'
})
</script>
</html>
image.png