Vue组件间通信的几种方式
- 0、前言
- 1、props/$emit(父子)
- 2、ref / $refs(父子)
- 3、provide / inject(深度父子)
- 4、EventBus 事件总线 (任意两个组件通讯)
- 5、$attrs / $listener(深度父子)
- 6、$parent / $children
- 7、vuex
- 7.1 Vuex原理
- 7.2 Vuex各模块功能
- 7.3 Vuex与localStorage
- 总结
0、前言
组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。一般来说,组件可以有以下几种关系:
种类:
- 父子组件通信;
- 兄弟组件通信;
- 跨级组件通信
1、props/$emit(父子)
父组件通过 props
向子组件传递数据,在子组件中就可以用this.xxx方式使用,子组件通过 $emit
和父组件通信。
- props的特点:
● props只能是父组件向子组件进行传值,props使得父子组件之间形成一个单向的下行绑定。子组件的数据会随着父组件的更新而响应式更新。
● props可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以是传递一个函数。
● props属性名规则:若在props中使用驼峰形式,模板中标签需要使用短横线的形式来书写。
- $emit的特点:
● $emit 绑定一个自定义事件,当这个事件被执行的时候就会将参数传递给父组件,而父组件通过v-on监听并接收参数。
// Parent.vue 组件
<template>
<div>
父组件
<child :msg="msg" @changeTitle="updateTitle"></child>
<h2>{{ parentTitle }}</h2>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
name: 'Parent',
components: { Child },
data () {
return {
msg: 'hello',
parentTitle: ''
}
},
methods: {
updateTitle (e) {
this.parentTitle = e
}
},
};
</script>
<style scoped></style>
// Child.vue 组件
<template>
<div>
子组件
{{ msg }}
<button @click="handleChildClick">子组件按钮</button>
</div>
</template>
<script>
export default {
name: 'Child',
props: {
msg: {
typeof: 'String',
required: true
}
},
data () {
return {
childTitle: '我是子组件传递过来的值'
}
},
methods: {
handleChildClick () {
this.$emit('changeTitle', this.childTitle)
}
},
};
</script>
<style scoped></style>
2、ref / $refs(父子)
可实现父子组件之间的通信,常用于父组件中调用子组件的方法或者获取子组件的属性。
// Parent.vue 组件
<template>
<div>
父组件
<child ref="child"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
name: 'Parent',
components: { Child },
data () {
return {
}
},
mounted () {
const child = this.$refs.child
console.log(child.name)
child.changeName('调用子组件')
},
};
</script>
<style scoped></style>
// Child.vue 组件
<template>
<div>
子组件
</div>
</template>
<script>
export default {
name: 'Child',
data () {
return {
name: '我是子组件',
}
},
methods: {
changeName (msg) {
console.log(msg)
}
},
};
</script>
<style scoped></style>
3、provide / inject(深度父子)
依赖注入,常见于插件或者组件库里。
多个组件嵌套时,顶层组件provide
提供变量,后代组件都可以通过inject
来注入变量。
缺点:传递的数据不是响应式的,inject
接收到数据后,provide
中的数据改变,但是后代组件中的数据不会改变。所以建议传一些常量或者方法。
// Parent.vue 组件
<template>
<div>
父组件
<child ref="child"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
name: 'Parent',
components: { Child },
data () {
return {
}
},
// 方式1 不能获取methods中对的方法
provide: {
name: '我是父组件的值'
}
// 方式2 不能获取data中的属性
provide () {
return {
name: '刘德华',
childMethod: this.changeTitle
}
},
methods: {
changeTitle () {
console.log('这是注册的方法')
}
},
};
</script>
<style scoped></style>
// Child.vue 组件
<template>
<div>
子组件
</div>
</template>
<script>
export default {
name: 'Child',
data () {
return {
}
},
inject: ['name', 'childMethod'],
mounted () {
console.log(this.name)
this.childMethod()
}
};
</script>
<style scoped></style>
4、EventBus 事件总线 (任意两个组件通讯)
用 $emit
去监听,用$on
去触发,注意需要$off
来取消监听,否则可能会造成内存泄漏。
对于比较小型的项目,没有必要引入 vuex 的情况下,可以使用 eventBus。相比我们上面说的所有通信方式,eventBus 可以实现任意两个组件间的通信。
// 方法1: 抽离出一个单独的js文件 Bus.js,然后在需要的地方引入
import Vue from 'vue'
export default new Vue()
// 方法2:直接挂载到全局main.js中
import Vue from 'vue'
Vue.prototype.$bus = new Vue()
// 方法3:注入到Vue跟对象上 main.js
import Vue from 'vue'
new Vue({
el: "#app",
data: {
Bus: new Vue()
}
})
// Parent.vue 组件
<template>
<div>
<child></child>
<button @click="handleClick">按钮</button>
</div>
</template>
<script>
import Bus from './Bus.js'
import Child from './Child.vue'
export default {
name: 'Parent',
components: { Child },
data () {
return {
}
},
methods: {
handleClick () {
// 自定义事件名
Bus.$emit("sendMsg", "发送的内容")
}
},
};
</script>
<style scoped></style>
// Child.vue 组件
<template>
<div>
</div>
</template>
<script>
import Bus from './Bus.js'
export default {
name: 'Child',
data () {
return {
}
},
mounted () {
// 监听事件的触发
Bus.$on('sendMsg', data => {
console.log('这是接受到的数据', data)
})
},
beforeDestroy () {
// 取消监听
Bus.$off("sendMsg")
}
};
</script>
<style scoped></style>
5、$attrs / $listener(深度父子)
多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs"
传入内部组件。通常配合 interitAttrs 选项一起使用。
$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners"
传入内部组件。
// Parent.vue组件:
<template>
<div>
<child :name="name" :age="age" :infoObj="infoObj" @updateInfo="updateInfo" @delInfo="delInfo" />
</div>
</template>
<script>
import Child from './Child.vue'
export default {
name: 'Parent',
components: { Child },
data () {
return {
name: '张三',
age: 30,
infoObj: {
from: '浙江',
job: 'policeman',
hobby: ['reading', 'writing', 'skating']
}
}
},
methods: {
updateInfo () {
console.log('update info')
},
delInfo () {
console.log('delete info')
}
}
};
</script>
<style scoped></style>
// Child.vue 组件
<template>
<!-- 通过 $listeners 将父作用域中的事件,传入 grandSon 组件,使其可以获取到 father 中的事件 -->
<grand-son :height="height" :weight="weight" @addInfo="addInfo" v-bind="$attrs" v-on="$listeners" />
</template>
<script>
import GrandSon from './GrandSon.vue'
export default {
name: 'Child',
components: { GrandSon },
props: ['name'],
data () {
return {
height: '180cm',
weight: '72kg'
}
},
created () {
console.log(this.$attrs)
// 结果:age, infoObj, 因为父组件共传来name, age, infoObj三个值,由于name被 props接收了,所以只有age, infoObj属性
console.log(this.$listeners) // updateInfo: f, delInfo: f
},
methods: {
addInfo () {
console.log('add info')
}
}
}
</script>
// GrandSon.vue 组件
<template>
<div>
{{ $attrs }} --- {{ $listeners }}
</div>
</template>
<script>
export default {
props: ['weight'],
created () {
console.log(this.$attrs) // age, infoObj, height
console.log(this.$listeners) // updateInfo: f, delInfo: f, addInfo: f
this.$emit('updateInfo') // 可以触发 father 组件中的updateInfo函数
}
}
</script>
6、$parent / $children
// Parent.vue 组件
<template>
<div class="hello_world">
<div>{{ msg }}</div>
<child></child>
<button @click="changeA">点击改变子组件值</button>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
name: 'Parent',
components: { Child },
data () {
return {
msg: 'hello'
}
},
methods: {
changeA () {
// 获取到子组件A
console.log(this.$children)
this.$children[0].messageA = 'this is new value'
}
}
};
</script>
<style scoped></style>
// Child.vue 组件
<template>
<div class="com_a">
<span>{{ messageA }}</span>
<p>获取父组件的值为: {{ parentVal }}</p>
</div>
</template>
<script>
export default {
name: 'Child',
data () {
return {
messageA: 'this is old'
}
},
computed: {
parentVal () {
return this.$parent.msg
}
}
}
</script>
7、vuex
7.1 Vuex原理
Vuex是实现组件全局状态(数据)管理的一种机制,可以方便实现组件数据之间的共享;Vuex集中管理共享的数据,易于开发和后期维护;能够高效的实现组件之间的数据共享,提高开发效率;存储在Vuex的数据是响应式的,能够实时保持页面和数据的同步。
7.2 Vuex各模块功能
Vuex重要核心属性包括:state,mutations,action,getters,modules。
Vuex主要包括以下几个核心模块:
- State:用于数据的存储,是store中的唯一数据源;
- Getter:在 store 中定义“getter”(可以认为是 store 的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
7.3 Vuex与localStorage
vuex 是 vue 的状态管理器,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到了初始状态,具体做法应该在vuex里数据改变的时候把数据拷贝一份保存到localStorage里面,刷新之后,如果localStorage里有保存的数据,取出来再替换store里的state。
let defaultCity = "杭州"
try { // 用户关闭了本地存储功能,此时在外层加个try...catch
if (!defaultCity){
defaultCity = JSON.parse(window.localStorage.getItem('defaultCity'))//在Vuex里保存的状态都是数组,而localStorage只支持字符串,所以需要用JSON转换
}
}catch(e){}
export default new Vuex.Store({
state: {
city: defaultCity
},
mutations: {
changeCity(state, city) {
state.city = city
try {
window.localStorage.setItem('defaultCity', JSON.stringify(state.city));
// 数据改变的时候把数据拷贝一份保存到localStorage里面
} catch (e) {}
}
}
})
这里需要注意的是:由于vuex里,我们保存的状态,都是数组,而localStorage只支持字符串,所以需要用JSON转换:
JSON.stringify(state.subscribeList); // array -> string
JSON.parse(window.localStorage.getItem("subscribeList")); // string -> array
总结
根据以上对这7种组件间的通信方法,可以将不同组件间的通信分为4种类型:父子组件间通信、跨代组件间通信、兄弟组件间通信、任意组件间通信
- 父子组件间通信
子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
通过 ref 属性给子组件设置一个名字。父组件通过$refs
组件名来获得子组件,子组件通过$parent
获得父组件,这样也可以实现通信。
使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。 - 跨代组件间通信
跨代组件间通信其实就是多层的父子组件通信,同样可以使用上述父子组件间通信的方法,只不过需要多层通信会比较麻烦。
使用上述的7种方法的$attrs / $listeners
方法。 - 兄弟组件间通信
通过$parent + $refs
以父组件为中间人来获取到兄弟组件,也可以进行通信。 - 任意组件间通信
使用eventBus
,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。