最全的Vue组件通信方式总结
1、一图认清组件关系名词
- 父子关系:a与b、a与c、b与d、c与e
- 兄弟关系:b与c
- 隔代关系:a与d、a与e
- 非直系亲属:d与e
总结为三大类:
- 父子组件之间通信
- 兄弟组件之间通信
- 跨级通信
2、8种通信方式及使用总结
-
props
/$emit
-
$children
/$parent
-
provide
/inject
-
ref
/refs
-
eventbus
-
vuex
-
localstorage
/sessionstorage
-
$attrs
与$listeners
常见使用场景可以分为三类:
-
父子组件通信:
props
;$parent
/$children
;provide
/inject
;ref
;$attrs
/$listeners
-
兄弟组件通信:
eventbus
; vuex -
跨级通信:
eventbus
;vuex;provide
/inject
、$attrs
/$listeners
3、8种通信方式详解
-
props
/$emit
-
1. 父组件向子组件传值
下面通过一个例子说明父组件如何向子组件传递数据:在子组件article.vue
中如何获取父组件section.vue
中的数据articles:['红楼梦', '西游记','三国演义']
// section父组件 <template> <div class="section"> <com-article :articles="articlelist"></com-article> </div> </template> <script> import comarticle from './test/article.vue' export default { name: 'helloworld', components: { comarticle }, data() { return { articlelist: ['红楼梦', '西游记', '三国演义'] } } } </script>
// 子组件 article.vue <template> <div> <span v-for="(item, index) in articles" :key="index">{{item}}</span> </div> </template> <script> export default { props: ['articles'] } </script>
总结: prop 只可以从上一级组件传递到下一级组件(父子组件),即所谓的单向数据流。而且 prop 只读,不可被修改,所有修改都会失效并警告。
-
2. 子组件向父组件传值
对于$emit
我自己的理解是这样的:$emit
绑定一个自定义事件, 当这个语句被执行时, 就会将参数arg传递给父组件,父组件通过v-on监听并接收参数。 通过一个例子,说明子组件如何向父组件传递数据。 在上个例子的基础上, 点击页面渲染出来的ariticle
的item
, 父组件中显示在数组中的下标// 父组件中 <template> <div class="section"> <com-article :articles="articlelist" @onemitindex="onemitindex"></com-article> <p>{{currentindex}}</p> </div> </template> <script> import comarticle from './test/article.vue' export default { name: 'helloworld', components: { comarticle }, data() { return { currentindex: -1, articlelist: ['红楼梦', '西游记', '三国演义'] } }, methods: { onemitindex(idx) { this.currentindex = idx } } } </script>
<template> <div> <div v-for="(item, index) in articles" :key="index" @click="emitindex(index)">{{item}}</div> </div> </template> <script> export default { props: ['articles'], methods: { emitindex(index) { this.$emit('onemitindex', index) } } } </script>
-
-
$children
/$parent
- 通过
$parent
和$children
就可以访问组件的实例,拿到实例代表什么?代表可以访问此组件的所有方法和data
。接下来就是怎么实现拿到指定组件的实例。// 父组件中 <template> <div class="hello_world"> <div>{{msg}}</div> <com-a></com-a> <button @click="changea">点击改变子组件值</button> </div> </template> <script> import coma from './test/coma.vue' export default { name: 'helloworld', components: { coma }, data() { return { msg: 'welcome' } }, methods: { changea() { // 获取到子组件a this.$children[0].messagea = 'this is new value' } } } </script>
// 子组件中 <template> <div class="com_a"> <span>{{messagea}}</span> <p>获取父组件的值为: {{parentval}}</p> </div> </template> <script> export default { data() { return { messagea: 'this is old' } }, computed:{ parentval(){ return this.$parent.msg; } } } </script>
要注意边界情况,如在#app
上拿$parent
得到的是new vue()
的实例,在这实例上再拿$parent
得到的是undefined
,而在最底层的子组件拿$children
是个空数组。也要注意得到$parent
和$children
的值不一样,$children
的值是数组,而$parent
是个对象 - 总结:上面两种方式用于父子组件之间的通信, 而使用props进行父子组件通信更加普遍; 二者皆不能用于非父子组件之间的通信
- 通过
-
provide
/inject
-
provide
/inject
是vue2.2.0
新增的api, 简单来说就是父组件中通过provide
来提供变量, 然后再子组件中通过inject
来注入变量。 - 注意: 这里不论子组件嵌套有多深, 只要调用了
inject
那么就可以注入provide
中的数据,而不局限于只能从当前父组件的props属性中回去数据 -
// a.vue <template> <div> <comb></comb> </div> </template> <script> import comb from '../components/test/comb.vue' export default { name: "a", provide: { for: "demo" }, components:{ comb } } </script>
// b.vue <template> <div> {{demo}} <comc></comc> </div> </template> <script> import comc from '../components/test/comc.vue' export default { name: "b", inject: ['for'], data() { return { demo: this.for } }, components: { comc } } </script>
// c.vue <template> <div> {{demo}} </div> </template> <script> export default { name: "c", inject: ['for'], data() { return { demo: this.for } } } </script>
-
-
ref
/refs
-
ref
:如果在普通的 dom 元素上使用,引用指向的就是 dom 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据, 我们看一个ref
来访问组件的例子:// 子组件 a.vue export default { data () { return { name: 'vue.js' } }, methods: { sayhello () { console.log('hello') } } }
// 父组件 app.vue <template> <component-a ref="coma"></component-a> </template> <script> export default { mounted () { const coma = this.$refs.coma; console.log(coma.name); // vue.js coma.sayhello(); // hello } } </script>
-
-
eventbus
-
eventbus
又称为事件总线,在vue中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件, 所以组件都可以通知其他组件。 - eventbus也有不方便之处, 当项目较大,就容易造成难以维护的灾难
- 在vue的项目中怎么使用
eventbus
来实现组件之间的数据通信呢?具体通过下面几个步骤 -
1. 初始化
// event-bus.js import vue from 'vue' export const eventbus = new vue()
-
2. 发送事件
<template> <div> <show-num-com></show-num-com> <addition-num-com></addition-num-com> </div> </template> <script> import shownumcom from './shownum.vue' import additionnumcom from './additionnum.vue' export default { components: { shownumcom, additionnumcom } } </script>
// addtionnum.vue 中发送事件 <template> <div> <button @click="additionhandle">+加法器</button> </div> </template> <script> import {eventbus} from './event-bus.js' console.log(eventbus) export default { data(){ return{ num:1 } }, methods:{ additionhandle(){ eventbus.$emit('addition', { num:this.num++ }) } } } </script>
-
3. 接收事件
// shownum.vue 中接收事件 <template> <div>计算和: {{count}}</div> </template> <script> import { eventbus } from './event-bus.js' export default { data() { return { count: 0 } }, mounted() { eventbus.$on('addition', param => { this.count = this.count + param.num; }) } } </script>
这样就实现了在组件
addtionnum.vue
中点击相加按钮, 在shownum.vue
中利用传递来的num
展示求和的结果. -
4. 移除事件监听者
如果想移除事件的监听, 可以像下面这样操作:import { eventbus } from 'event-bus.js' eventbus.$off('addition', {})
-
-
vuex
-
1. vuex介绍
vuex 是一个专为 vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化. vuex 解决了
多个视图依赖于同一状态
和来自不同视图的行为需要变更同一状态
的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上2. vuex各个模块
-
state
:用于数据的存储,是store中的唯一数据源 -
getters
:如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算 -
mutations
:类似函数,改变state数据的唯一途径,且不能用于处理异步事件 -
actions
:类似于mutation
,用于提交mutation
来改变状态,而不直接变更状态,可以包含任意异步操作 -
modules
:类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护
3. vuex实例应用
// 父组件 <template> <div id="app"> <childa/> <childb/> </div> </template> <script> import childa from './components/childa' // 导入a组件 import childb from './components/childb' // 导入b组件 export default { name: 'app', components: {childa, childb} // 注册a、b组件 } </script>
// 子组件childa <template> <div id="childa"> <h1>我是a组件</h1> <button @click="transform">点我让b组件接收到数据</button> <p>因为你点了b,所以我的信息发生了变化:{{bmessage}}</p> </div> </template> <script> export default { data() { return { amessage: 'hello,b组件,我是a组件' } }, computed: { bmessage() { // 这里存储从store里获取的b组件的数据 return this.$store.state.bmsg } }, methods: { transform() { // 触发receiveamsg,将a组件的数据存放到store里去 this.$store.commit('receiveamsg', { amsg: this.amessage }) } } } </script>
// 子组件 childb <template> <div id="childb"> <h1>我是b组件</h1> <button @click="transform">点我让a组件接收到数据</button> <p>因为你点了a,所以我的信息发生了变化:{{amessage}}</p> </div> </template> <script> export default { data() { return { bmessage: 'hello,a组件,我是b组件' } }, computed: { amessage() { // 这里存储从store里获取的a组件的数据 return this.$store.state.amsg } }, methods: { transform() { // 触发receivebmsg,将b组件的数据存放到store里去 this.$store.commit('receivebmsg', { bmsg: this.bmessage }) } } } </script>
vuex的store.js
import vue from 'vue' import vuex from 'vuex' vue.use(vuex) const state = { // 初始化a和b组件的数据,等待获取 amsg: '', bmsg: '' } const mutations = { receiveamsg(state, payload) { // 将a组件的数据存放于state state.amsg = payload.amsg }, receivebmsg(state, payload) { // 将b组件的数据存放于state state.bmsg = payload.bmsg } } export default new vuex.store({ state, mutations })
vuex 是 vue 的状态管理器,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到了初始状态,具体做法应该在vuex里数据改变的时候把数据拷贝一份保存到localstorage里面,刷新之后,如果localstorage里有保存的数据,取出来再替换store里的state。let defaultcity = "上海" try { // 用户关闭了本地存储功能,此时在外层加个try...catch if (!defaultcity){ defaultcity = json.parse(window.localstorage.getitem('defaultcity')) } }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
-
-
-
localstorage
/sessionstorage
- 这种通信比较简单,缺点是数据和状态比较混乱,不太容易维护。 通过
window.localstorage.getitem(key)
获取数据 通过window.localstorage.setitem(key,value)
存储数据 - 注意用
json.parse()
/json.stringify()
做数据格式转换localstorage
/sessionstorage
可以结合vuex
, 实现数据的持久保存,同时使用vuex解决数据和状态混乱问题.
- 这种通信比较简单,缺点是数据和状态比较混乱,不太容易维护。 通过
-
$attrs
与$listeners
-
-
现在我们来讨论一种情况, 我们一开始给出的组件关系图中a组件与d组件是隔代关系, 那它们之前进行通信有哪些方式呢?
- 使用
props
绑定来进行一级一级的信息传递, 如果d组件中状态改变需要传递数据给a, 使用事件系统一级级往上传递 - 使用
eventbus
,这种情况下还是比较适合使用, 但是碰到多人合作开发时, 代码维护性较低, 可读性也低 - 使用vuex来进行数据管理, 但是如果仅仅是传递数据, 而不做中间处理,使用vuex处理感觉有点大材小用了.
在
vue2.4
中,为了解决该需求,引入了$attrs
和$listeners
, 新增了inheritattrs
选项。 在版本2.4以前,默认情况下,父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外),将会“回退”且作为普通的html特性应用在子组件的根元素上。 - 使用
-
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritattrs 选项一起使用。$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件 -
接下来看一个跨级通信的例子:
// app.vue // index.vue <template> <div> <child-com1 :name="name" :age="age" :gender="gender" :height="height" title="程序员成长指北" ></child-com1> </div> </template> <script> const childcom1 = () => import("./childcom1.vue"); export default { components: { childcom1 }, data() { return { name: "zhang", age: "18", gender: "女", height: "158" }; } }; </script>
// childcom1.vue <template class="border"> <div> <p>name: {{ name}}</p> <p>childcom1的$attrs: {{ $attrs }}</p> <child-com2 v-bind="$attrs"></child-com2> </div> </template> <script> const childcom2 = () => import("./childcom2.vue"); export default { components: { childcom2 }, inheritattrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性 props: { name: string // name作为props属性绑定 }, created() { console.log(this.$attrs); // { "age": "18", "gender": "女", "height": "158", "title": "程序员成长" } } }; </script>
// childcom2.vue <template> <div class="border"> <p>age: {{ age}}</p> <p>childcom2: {{ $attrs }}</p> </div> </template> <script> export default { inheritattrs: false, props: { age: string }, created() { console.log(this.$attrs); // { "gender": "女", "height": "158", "title": "程序员成长" } } }; </script>
-
额外补充:
v-model
父组件通过v-model传递值给子组件时,会自动传递一个value的prop属性,
子组件中通过this.$emit(‘input',val)自动修改v-model绑定的值,下面看个例子。
父组件:
<template> <div> <child v-model="total"></child> <button @click="increse">增加5</button> </div> </template> <script> import child from "./child.vue" export default { components: { child }, data: function () { return { total: 0 }; }, methods: { increse: function () { this.total += 5; } } } </script>
子组件:
<template> <div> <span>{{value}}</span> <button @click="reduce">减少5</button> </div> </template> <script> export default { props: { value: number // 注意这里是value }, methods: { reduce: function(){ this.$emit("input", this.value - 5) // 事件为input } } } </script>
下一篇: java知识随笔整理-标量函数和表值函数