Vue的通信方式有几种?隔代组件的通信用哪种方式解决?
一:props/$emit
父传子:
子传父 $emit
// 父组件 <s-child v-on:infos="cy" /> 与子组件infos事件保持一致 <div>{{msg}}</div> data() { msg: '' } methods: { cy(somedata) { this.msg = somedata } } // 子组件 <button v-on:click="onClick">向父组件传参</button> data() { return { childmsg: "我是子组件的值" } } methods: { onClick: function () { this.$emit('infos', this.childmsg) // infos是父组件绑定的事件,this.childmsg作为参数传过去 } }
二:$children/$parent
// parent <template> <div> <div> <div>{{ msg }}</div> <s-child></s-child> <button @click="changeA">点击改变子组件值</button> </div> </div> </template> <script> import Child from './child' export default { name: 'ThisParent', data() { return { // message: 'hello' msg: 'Welcome parents' } }, components: { "s-child": Child }, methods: { changeA() { // 获取到子组件A this.$children[0].messageA = 'this is new value for parents' } } } </script>
// child <template> <div> <div class="com_a"> <span>{{ messageA }}</span> <p>获取父组件的值为:{{ parentVal }}</p> </div> </div> </template> <script> export default { data() { return { messageA: 'this is old' } }, computed:{ parentVal() { return this.$parent.msg; } } } </script>
三:provide/inject
概念: provide
/ inject
是vue2.2.0
新增的api, 简单来说就是父组件中通过provide
来提供变量, 然后再子组件中通过inject
来注入变量。注意: 这里不论子组件嵌套有多深, 只要调用了inject
那么就可以注入provide
中的数据,而不局限于只能从当前父组件的props属性中回去数据
// A.vue <template> <div> <com-b></com-b> </div> </template> <script> import comB from './B' export default { name: 'A', provide: { for: "A里面的值" }, components: { comB } } </script> // B.vue <template> <div> {{demo}} <com-c></com-c> </div> </template> <script> import comC from './C' 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
来访问组件的例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app-7"> <div> <to-child ref='ch'></to-child> <h1 @click="onclick">我是父组件点击获取子组件信息</h1> </div> </div> </body> <script> Vue.component('to-child', { template: '<div>我是子组件</div>', data() { return { sonData: '我是子组件的数据' } }, methods: { sonMethod() { console.log('我是子组件的方法sonMethod') } } }) var app7 = new Vue({ el: '#app-7', data() { return { } }, methods: { onclick() { // 父组件可以通过$refs拿到子组件的对象 // 然后直接调用子组件里面methods方法和data数据 let chil = this.$refs.ch console.log(chil) // 子组件对象 console.log(chil.sonData) // 子组件数据 console.log(chil.sonMethod()) // 子组件的方法 } } }) </script> </html>
五:eventBus
EventBus又称为事件总线。在Vue中可以使用EventBus来作为组件传递数据的桥梁,就像是所有组件共用相同的事件中心,可以向该中心注册发送或者接收事件,所以组件都可以上下平行通知其它组件。但如果使用不慎,就会造成维护灾难。因此推荐更加强大的Vuex作为状态管理中心,将通知概念上升到共享状态层次。
使用 EventBus
初始化:
// 第一种方式:可以在main.js 中,初始化 EventBus Vue.prototype.$EventBus = new Vue() // 第二种方式:创建一个Bus.js ,再创建事件总线并将其导出,以便其它模板可以使用或者监听 // Bus.js import Vue from 'vue' export const EventBus = new Vue(); // 你需要做的只是引入 Vue 并导出它的一个实例(在这种情况下,我称它为 EventBus )。 // 实质上它是一个不具备DOM的组件,它具有的仅仅只是它实例方法而已,因此它非常的轻便。
这样我们就创建了EventBus,接下来只需要在组件中加载它,并调用同一个方法。
发送和接收事件
和父子组件通信差不多,用 EventBus.$emit('emit事件名',数据)
发送, EventBus.$on("emit事件名", callback(payload1,…))
接受
// additionNum.vue 中发送事件 <template> <div> <button v-on:click="additionHandle">+加法</button> </div> </template> <script> import {EventBus} from './bus.js' console.log(EventBus) export default { data() { return{ num: 1 } }, methods: { additionHandle() { EventBus.$emit('addition', { num: this.num++ }) } } } </script> // showNum.vue 中接收事件 <template> <div> <div>计算和:{{count}}</div> </div> </template> <script> import {EventBus} from './bus.js' export default { data() { return{ count: 0 } }, mounted() { EventBus.$on('addition', param => { this.count = param.num; }) } } </script>
如果只需要监听(接收)一次数据可以使用 EventBus.$once('事件名', callback(payload1,...)
移除事件监听者
EventBus.$off('事件名',回调函数)
-
EventBus.$off('事件名', callback)
,只移除这个回调的监听器。 -
EventBus.$off('事件名')
,移除该事件所有的监听器。 -
EventBus.$off()
, 移除所有的事件监听器,注意不需要添加任何参数。
// 导入我们刚刚创建的 EventBus import { EventBus } from '../Bus.js' // 事件监听函数 const clickHandler = function(clickCount) { console.log(`Oh, hello)`) } // 开始监听事件 EventBus.$on('i-got-clicked', clickHandler); // 停止监听 EventBus.$off('i-got-clicked', clickHandler);
全局EventBus
全局EventBus,虽然在某些示例中不提倡使用,但它是一种非常漂亮且简单的方法,可以跨组件之间共享数据。
它的工作原理是发布/订阅方法,通常称为 Pub/Sub 。
由于是全局的,必然所有事件都订阅它, 所有组件也发布到它,订阅组件获得更新。也就是说所有组件都能够将事件发布到总线,然后总线由另一个组件订阅,然后订阅它的组件将得到更新。
创建全局EventBus
全局事件总线只不过是一个简单的 vue 组件。
var EventBus = new Vue(); Object.defineProperties(Vue.prototype, { $bus: { get: function () { return EventBus } } })
使用 on和emit
在这个特定的总线中使用两个方法。一个用于创建发出的事件,它就是emit: 另一个用于订阅on:
this.#bus.$emit('事件名', {...pass some event data ...}); this.$bus.$on('事件名', ($event) => {})
EventBus的优缺点
缺点:
- vue是单页面应用,在某个页面刷新,与之相关的EventBus会被移除,这样会导致业务走不下去。
- 如果业务有反复操作的页面,EventBus在监听的时候会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理EventBus在项目中的关系。通常会用到,在vue页面销毁时,同事移除EventBus事件监听。
- 由于是都使用一个Vue实例,所以容易出现重复触发的情景,两个页面都定义了同一个事件名,并且没有用$off销毁常出现在路由切换时)。
优点
-
解决了多层组件之间繁琐的事件传播。
-
使用原理十分简单,代码量少。
六:Vuex
1.Vuex介绍
Vuex是一个专门为vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex解决了多个视图依赖同一状态和来自不同视图的行为需要变更同一状态的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上。
2.Vuex各个模块
1. state:用于数据的存储,是store中唯一数据源
2. getters:如Vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算
2. mutations:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。一条重要的原则就是要记住 mutation 必须是同步函数。你可以在组件中使用 this.$store.commit('xxx')
提交 mutation
3. actions:类是mutation,用于提交mutation来改变状态,而不是直接变更状态,可以包含任意异步操作
4. modules类是命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护
3.Vuex实例应用
// 父组件 <template> <div id="app"> <child-a></child-a> <child-b></child-b> </div> </template> <script> import ChildA from './childA'; import ChildB from './childB'; export default { name: 'App', components: { ChildA, ChildB } } </script> // 子组件 <template> <div id="childA"> <h1>我是组件A</h1> <button @click="transform">点我让B组件接收到数据</button> <p>因为你点了B,所以我的信息发生了变化:{{ BMessage }}</p> </div> </template> <script> export default { name: 'childA', 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> // 子组件B <template> <div id="childB"> <h1>我是组件B</h1> <button @click="transform">点我让A组件接收到数据</button> <p>因为你点了A,所以我的信息发生了变化:{{ AMessage }}</p> </div> </template> <script> export default { name: 'childA', data() { return { BMessage: 'Hello, A组件, 我是B组件' } }, computed: { AMessage() { // 这里存储从store里获取的A组件的数据 return this.$store.state.AMsg } }, methods: { transform() { // 触发receiveAMsg,将B组件的数据存放在store里去 this.$store.commit('receiveBMsg', { BMsg: this.BMessage }) } } } </script> // Vuex的 store.js import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex) export const store = new Vuex.Store({ strict: true, state: { AMsg: '', BMsg: '' }, getters: {}, mutations: { receiveAMsg(state, payload) { // 将A组件的数据存放于state state.AMsg = payload.AMsg }, receiveBMsg(state, payload) { // 将B组件的数据存放于state state.BMsg = payload.BMsg } }, actions: { }, modules: { } });
以上还是很浅,深度有待挖掘。
七: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处理感觉有点大材小用了.
-
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。 -
$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
// index.vue <template> <div> <h2>浪里行舟</h2> <child-com1 :foo="foo" :boo="boo" :coo="coo" :doo="doo" title="前端工匠" ></child-com1> </div> </template> <script> const childCom1 = () => import("./childCom1.vue"); export default { components: { childCom1 }, data() { return { foo: "Javascript", boo: "Html", coo: "CSS", doo: "Vue" }; } }; </script> // childCom1.vue <template class="border"> <div> <p>foo: {{ foo }}</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: { foo: String // foo作为props属性绑定 }, created() { console.log(this.$attrs); // { "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" } } }; </script> // childCom2.vue <template> <div class="border"> <p>boo: {{ boo }}</p> <p>childCom2: {{ $attrs }}</p> <child-com3 v-bind="$attrs"></child-com3> </div> </template> <script> const childCom3 = () => import("./childCom3.vue"); export default { components: { childCom3 }, inheritAttrs: false, props: { boo: String }, created() { console.log(this.$attrs); // { "coo": "CSS", "doo": "Vue", "title": "前端工匠" } } }; </script> // childCom3.vue <template> <div class="border"> <p>childCom3: {{ $attrs }}</p> </div> </template> <script> export default { props: { coo: String, title: String } }; </script>
关于$listeners:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> 父组件 <c-child v-on:todo="handleClick"></c-child> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.7/vue.common.dev.js"></script> </body> <script> let vm = new Vue({ el: '#app', data: { }, methods: { handleClick () { console.log('父组件的点击事件') } }, components: { 'CChild': { template: `<button v-on:click="$listeners.todo">子组件的按钮B</button>`, // 子组件绑定click事件,通过 $listeners 调取父组件的 todo 事件 created() { console.log(this.$listeners) // 包含父级所有绑定的方法 } }, } }) </script> </html>
如上图所示$attrs
表示没有继承数据的对象,格式为{属性名:属性值}。Vue2.4提供了$attrs
, $listeners
来传递数据与事件,跨级组件之间的通讯变得更简单。
简单来说:$attrs
与$listeners
是两个对象,$attrs
里存放的是父组件中绑定的非 Props 属性,$listeners
里存放的是父组件中绑定的非原生事件。
九:slot插槽方式
总结
常见使用场景可以分为三类:
- 父子组件通信:
props
;$parent
/$children
;provide
/inject
;ref
;$attrs
/$listeners
- 兄弟组件通信:
eventBus
; vuex - 跨级通信:
eventBus
;Vuex;provide
/inject
、$attrs
/$listeners
参考引用链接: