vue是数据驱动视图更新的框架, 所以对于vue来说组件间的数据通信非常重要;我们常用的方式莫过于通过props传值给子组件,但是vue还有其他很多不常用的通信方式,了解他们,也许在以后在写代码的时候能给你带来更多的思路和选择。
prop/$emit
父组件通过prop
的方式向子组件传递数据,而通过$emit
子组件可以向父组件通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <template> <div> 当前选中:{{ current }} <Child :list="list" @change="changeCurrent"></Child> </div> </template> <script> import Child from "./child"; export default { data() { return { current: 0, list: ["红楼梦", "水浒传", "三国演义", "西游记"] }; }, components: { Child }, methods: { changeCurrent(num) { this.current = num; } } }; </script>
|
我们可以通过prop
向子组件传递数据;用一个形象的比喻来说,父子组件之间的数据传递相当于自上而下的下水管子,管子中的水就像数据,水只能从上往下流,不能逆流。这也正是Vue的设计理念之单向数据流。而prop
正是管道与管道之间的一个衔接口,这样水(数据)才能往下流。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <div> <template v-for="(item, index) in list"> <div @click="clickItem(index)" :key="index">{{ item }}</div> </template> </div> </template> <script> export default { props: { list: { type: Array, default: () => { return []; } } }, methods: { clickItem(index) { this.$emit("change", index); } } }; </script>
|
在子组件中我们通过props对象定义了接收父组件值的类型和默认值,然后通过$emit()
触发父组件中的自定义事件。prop/$emit
传递数据的方式在日常开发中用的非常多,一般涉及到组件开发都是基于通过这种方式;通过父组件中注册子组件,并在子组件标签上绑定对自定义事件的监听。他的优点是传值取值方便简洁明了,但是这种方式的缺点是:
- 由于数据是单向传递,如果子组件需要改变父组件的props值每次需要给子组件绑定对应的监听事件。
- 如果父组件需要给孙组件传值,需要子组件进行转发,较为不便。
.sync修饰符
有些情况下,我们希望在子组件能够“直接修改”父组件的prop值,但是双向绑定会带来维护上的问题;vue提供了一种解决方案,通过语法糖.sync修饰符。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
.sync修饰符在 vue1.x
的时候曾作为双向绑定功能存在,即子组件可以修改父组件中的值。但是它违反了单向数据流的设计理念,所以在 vue2.0
的时候被干掉了。但是在 vue2.3.0+
以上版本又重新引入了。但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的v-on
监听器。说白了就是让我们手动进行更新父组件中的值了,从而使数据改动来源更加的明显。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div> <Child :msg.sync="msg" :num.sync="num"></Child> </div> </template> <script> import Child from "./child"; export default { name: "way2", components: { Child }, data() { return { msg: "hello every guys", num: 0 }; } }; </script>
|
我们在Child组件传值时给每个值添加一个.sync修饰,在编译时会被扩展为如下代码:
| <Child :msg="msg" @update.msg="val => msg = val" :num.sync="num" @update.num="val => num = val"></Child>
|
因此子组件中只需要显示的触发update的更新事件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <template> <div> <div @click="clickRevert">点击更新字符串:{{ msg }}</div> <div>当前值:{{ num }}</div> <div @click="clickOpt('add')" class="opt">+</div> <div @click="clickOpt('sub')" class="opt">-</div> </div> </template> <script> export default { props: { msg: { type: String, default: "" }, num: { type: Number, default: 0 } }, methods: { clickRevert() { let { msg } = this; this.$emit("update:msg",msg.split("").reverse().join("")); }, clickOpt(type = "") { let { num } = this; if (type == "add") { num++; } else { num--; } this.$emit("update:num", num); } } }; </script>
|
这种“双向绑定”的操作是不是看着似曾相识?是的,v-model本质上也是一种语法糖,只不过它触发的不是update方法而是input方法;而且v-model没有.sync来的更加灵活,v-model只能绑定一个值。
总结:.sync修饰符优化了父子组件通信的传值方式,不需要在父组件再写多余的函数来修改赋值。
$attrs和$listeners
当需要用到从A到C的跨级通信时,我们会发现prop传值非常麻烦,会有很多冗余繁琐的转发操作;如果C中的状态改变还需要传递给A,使用事件还需要一级一级的向上传递,代码可读性就更差了。
因此vue2.4+
版本提供了新的方案:$attrs和$listeners
,我们先来看一下官网对$attrs的描述:
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件——在创建高级别的组件时非常有用。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
这一大段话第一次读非常的绕口,而且晦涩难懂,不过没关系,我们直接上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <template> <div> <Child :notUse="'not-use'" :childMsg="childMsg" :grandChildMsg="grandChildMsg" @onChildMsg="onChildMsg" @onGrandChildMsg="onGrandChildMsg" ></Child> </div> </template> <script> import Child from "./child"; export default { data() { return { childMsg: "hello child", grandChildMsg: "hello grand child" }; }, components: { Child }, methods: { onChildMsg(msg) { this.childMsg = msg; }, onGrandChildMsg(msg) { this.grandChildMsg = msg; } } }; </script>
|
我们首先定义了两个msg,一个给子组件展示,另一个给孙组件展示,首先将这两个数据传递到子组件中,同时将两个改变msg的函数传入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <template> <div class="box"> <div @click="clickMsg">{{ childMsg }}</div> <div>$attrs: {{ $attrs }}</div> <GrandChild v-bind="$attrs" v-on="$listeners"></GrandChild> </div> </template> <script> import GrandChild from "./grand-child"; export default { props: { childMsg: { type: String } }, methods: { clickMsg() { let { childMsg } = this; this.$emit( "onChildMsg", childMsg.split("").reverse().join("") ); } }, components: { GrandChild } }; </script>
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
在子组件中我们通过props获取子组件所需要的参数,即childMsg;剩余的参数就被归到了$attrs
对象中,我们可以在页面中展示出来,然后把它继续往孙组件中传;同时把所有的监听函数归到$listeners,也继续往下传。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="box1" @click="clickMsg">grand-child:{{ grandChildMsg }}</div> </template> <script> export default { props: { grandChildMsg: { type: String } }, methods: { clickMsg() { let { grandChildMsg } = this; this.$emit( "onGrandChildMsg", grandChildMsg.split("").reverse().join("") ); } } }; </script>
|
在孙组件中我们继续取出所需要的数据进行展示或者操作,运行结果如下:
当我们在组件上赋予一个非prop声明时,比如child组件上的notuse和grandchildmsg属性我们没有用到,编译之后的代码会把这个属性当成原始属性对待,添加到html原生标签上,所以我们查看代码是这样的:
这样会很难看,我们可以在组件上加上inheritAttrs
属性将它去掉:
| export default { mounted(){}, inheritAttrs: false, }
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
总结:$attrs和$listeners很好的解决了跨一级组件传值的问题。
provide和inject
虽然$attrs和$listeners可以很方便的从父组件传值到孙组件,但是如果跨了三四级,并且想要的数据已经被上级组件取出来,这时$attrs就不能解决了。
provide/inject是vue2.2+
版本新增的属性,简单来说就是父组件中通过provide来提供变量, 然后再子组件中通过inject来注入变量。这里inject注入的变量不像$attrs
,只能向下一层;inject不论子组件嵌套有多深,都能获取到。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <template> <div> <Child></Child> </div> </template> <script> import Child from "./child"; export default { components: { Child }, data() { return { childmsg: "hello child", grandmsg: "hello grand child" }; }, provide() { return { childmsg: this.childmsg, grandmsg: this.grandmsg }; }, mounted() { setTimeout(() => { this.childmsg = "hello new child"; this.grandmsg = "hello new grand child"; }, 2000); }, }; </script>
|
我们在父组件通过provide注入了两个变量,并且在两秒之后修改变量的值,然后就在子组件和孙组件取出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <template> <div class="box"> <div>child-msg:{{ childmsg }}</div> <div>grand-msg:{{ grandmsg }}</div> <GrandChild></GrandChild> </div> </template> <script> import GrandChild from "./grand-child"; export default { inject: ["childmsg", "grandmsg"], components: { GrandChild }, }; </script>
<template> <div class="box"> <div>child-msg:{{ childmsg }}</div> <div>grand-msg:{{ grandmsg }}</div> </div> </template> <script> export default { name: "GrandChild", inject: ["childmsg", "grandmsg"], }; </script>
|
可以看到子组件和孙组件都能取出值,并且渲染出来。需要注意的是,一旦子组件注入了某个数据,在data中就不能再声明这个数据了。
同时,过了两秒后我们发现childmsg和grandmsg的值并没有按照预期的改变,也就是说子组件并没有响应修改后的值,官网的介绍是这么说的:
提示:provide
和 inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
vue并没有把provide和inject设计成响应式的,这是vue故意的,但是如果传入了一个可监听的对象,那么就可以响应了:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export default { data() { return { respondData: { name: "hello respond" } }; }, provide() { return { respondData: this.respondData }; }, mounted() { setTimeout(() => { this.respondData.name = this.respondData.name .split("") .reverse() .join(""); }, 2000); }, }
|
那么为什么上面的props和$attrs都是响应式的,连破坏“单向数据流”的.sync
修饰符都是响应式的,但到了provide/inject就不是响应式的了呢?在网上找了半天的资料也没有找到确切的答案,本文就此结束。
就这么结束了吗?当然没有!在一(zi)个(ji)哥(xue)们(xi)的帮(yuan)助(ma)下,我总算找到了答案。首先我们试想一下,如果有多个子组件同时依赖于一个父组件提供的数据,那么一旦父组件修改了该值,那么所有组件都会受到影响,这是我们不希望看到的;这一方面增加了耦合度,另一方面使得数据变化不可控制。接着看一下vue是怎么来实现provide/inject的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } } export function initInjections (vm: Component) { const result = resolveInject(vm.$options.inject, vm) if (result) { observerState.shouldConvert = false Object.keys(result).forEach(key => { defineReactive(vm, key, result[key]) }) observerState.shouldConvert = true } } export function resolveInject (inject: any, vm: Component): ?Object { if (inject) { const result = Object.create(null) const keys = hasSymbol ? Reflect.ownKeys(inject).filter(key => { return Object.getOwnPropertyDescriptor(inject, key).enumerable }) : Object.keys(inject)
for (let i = 0; i < keys.length; i++) { const key = keys[i] const provideKey = inject[key].from let source = vm while (source) { if (source._provided && provideKey in source._provided) { result[key] = source._provided[provideKey] break } source = source.$parent } if (!source) { if ('default' in inject[key]) { const provideDefault = inject[key].default result[key] = typeof provideDefault === 'function' ? provideDefault.call(vm) : provideDefault } } } return result } }
|
可以看到初始化provide的时候将父组件的provide挂载到_provided
,但它不是一个响应式的对象;然后子组件通过$parent
向上查找所有父组件的_provided
获取第一个有目标属性的值,然后遍历绑定到子组件上;因为只是初始化的时候绑定的,而且_provided
也不是响应式的,所以造成了provide/inject的这种特性。
那么provide/inject这么危险,又不是响应式的,它能拿来做什么呢?打开element-ui
的源码搜索provide,我们可以看到非常多的组件使用了provide/inject,我们就拿form、form-item和button举个例子。
form和form-item都可以传入一个属性size来控制子组件的尺寸,但是子组件的位置是不固定的,可能会嵌套了好几层el-row或者el-col,如果一层一层的通过props传size下去会很繁琐,这是provide/inject就派上用处了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| export default { provide() { return { elFormItem: this }; }, }
export default { inject: { elForm: { default: '' }, elFormItem: { default: '' } }, computed: { _elFormItemSize() { return (this.elFormItem || {}).elFormItemSize; }, buttonSize() { return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size; }, }, }
|
我们通过父组件将elFormItem本身注入到子组件中,子组件通过inject获取父组件本身然后动态地计算buttonSize。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
总结:provide/inject能够解决多层组件嵌套传值的问题,但是是非响应的,即provide与inject之间没有绑定,注入的值是在子组件初始化过程中决定的。
EventBus
EventBus
我刚开始直接翻译理解为事件车
,但比较官方的翻译是事件总线
。它的实质就是创建一个vue实例,通过一个空的vue实例作为桥梁实现vue组件间的通信。它是实现非父子组件通信的一种解决方案,所有的组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的“灾难”。
| import Vue from "vue"; export default new Vue();
|
首先创造一个空的vue对象并将其导出,他是一个不具备DOM
的组件,它具有的仅仅只是它实例方法而已,因此它非常的轻便。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
| import bus from "@/utils/event-bus"; Vue.prototype.$bus = bus;
|
将其挂载到全局,变成全局的事件总线,这样在组件中就能很方便的调用了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div class="box"> <Child1></Child1> <Child2></Child2> </div> </template> <script> import Child1 from "./child1"; import Child2 from "./child2"; export default { components: { Child1, Child2 } }; </script>
|
我们先定义了两个子组件child1和child2,我们希望这两个组件能够直接给对方发送消息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| <template> <div> <div class="send" @click="clickSend">发送消息</div> <template v-for="(item, index) in msgList"> <div :key="index">{{ item }}</div> </template> </div> </template> <script> export default { data() { return { msgList: [] }; }, mounted() { this.$bus.$on("getMsg1", res => { this.msgList.push(res); }); }, methods: { clickSend() { this.$bus.$emit("getMsg2", "hello from1:" + parseInt(Math.random() * 20)); } } }; </script>
<template> <div> <div class="send" @click="clickSend">发送消息</div> <template v-for="(item, index) in msgList"> <div :key="index">{{ item }}</div> </template> </div> </template> <script> export default { data() { return { msgList: [] }; }, mounted() { this.$bus.$on("getMsg2", res => { this.msgList.push(res); }); }, methods: { clickSend() { this.$bus.$emit("getMsg1", "hello from2:" + parseInt(Math.random() * 20)); } } }; </script>
|
我们初始化时在child1和child2中分别注册了两个接收事件,然后点击按钮时分别触发这两个自定义的事件,并传入数据,最后两个组件分别能接收到对方发送的消息,最后效果如下:
前面也提到过,如果使用不善,EventBus会是一种灾难,到底是什么样的“灾难”了?大家都知道vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去。还要就是如果业务有反复操作的页面,EventBus在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理EventBus在项目中的关系。通常会用到,在页面或组件销毁时,同时移除EventBus事件监听。
| export default{ destroyed(){ $EventBus.$off('event-name') } }
|
总结:EventBus可以用来很方便的实现兄弟组件和跨级组件的通信,但是使用不当时也会带来很多问题;所以适合逻辑并不复杂的小页面,逻辑复杂时还是建议使用vuex。
vuex
在vue组件开发中,经常会遇到需要将当前组件的状态传递给其他非父子组件组件,或者一个状态需要共享给多个组件,这时采用上面的方式就会非常麻烦。vue提供了另一个库vuex来解决数据传递的问题;刚开始上手会感觉vuex非常的麻烦,很多概念也容易混淆,不过不用担心,本文不深入讲解vuex。
vuex实现了单向的数据流,在全局定义了一个State对象用来存储数据,当组件要修改State中的数据时,必须通过Mutation进行操作。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| export default { namespaced: true, state: { num: 1 }, mutations: { ADD_NUM(state) { state.num = state.num + 1; }, SUB_NUM(state) { state.num = state.num - 1; } }, actions: { ADD_SYNC({ commit }) { setTimeout(() => { commit("ADD_NUM"); }, 1000); }, SUB_SYNC({ commit }) { setTimeout(() => { commit("SUB_NUM"); }, 1000); } } };
import count from "./count"; export default new Vuex.Store({ modules: { count }, });
|
我们首先在全局定义了count.js
模块用来存放数据和修改数据的方法,然后在全局引入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <template> <div> <div>当前:{{ num }}</div> <div class="opt" @click="clickAdd">+</div> <div class="opt" @click="clickSub">-</div> <div class="opt" @click="clickAddSync">a+</div> <div class="opt" @click="clickSubSync">a-</div> </div> </template> <script> export default { name: "Child", computed: { num() { return this.$store.state.count.num; } }, methods: { clickAdd() { this.$store.commit("count/ADD_NUM"); }, clickSub() { this.$store.commit("count/SUB_NUM"); }, clickAddSync() { this.$store.dispatch("count/ADD_SYNC"); }, clickSubSync() { this.$store.dispatch("count/SUB_SYNC"); } } }; </script>
|
我们就可以在任何组件中来调用mutations和actions中的方法操作数据了。vuex在数据传值和操作数据维护起来比较方便,但是有一定的学习成本。
$refs
有时候我们需要在vue中直接来操作DOM元素,比如获取DIV的高度,或者直接调用子组件的一些函数;虽然原生的JS也能获取到,但是vue为我们提供了更方便的一个属性:$refs
。如果在普通的DOM元素上使用,获取到的就是DOM元素;如果用在子组件上,获取的就是组件的实例对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <div>初始化:{{ num }}</div> </template> <script> export default { data() { return { num: 0 }; }, methods: { addNum() { this.num += 1; }, subNum() { this.num -= 1; } } }; </script>
|
我们首先创建一个简单的子组件,有两个函数用来增减num的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template> <div> <Child ref="child"></Child> <div class="opt" ref="opt_add" @click="clickAddBtn">+</div> <div class="opt" ref="opt_sub" @click="clickSubBtn">-</div> <div class="opt" ref="opt_show" @click="clickShowBtn">show</div> </div> </template> <script> import Child from "./child"; export default { components: { Child }, data() { return {}; }, methods: { clickAddBtn() { this.$refs.child.addNum(); }, clickSubBtn() { this.$refs.child.subNum(); }, clickShowBtn() { console.log(this.$refs.child); console.log(this.$refs.child.num); } } }; </script>
|
我们给子组件增加一个ref属性child,然后通过$refs.child
来获取子组件的实例,通过实例来调用子组件中的函数。
可以看到我们获取到的是一个VueComponent
对象,这个对象包括了子组件的所有数据和函数,可以对子组件进行一些操作。
$parent和$children
如果页面有多个相同的子组件需要操作的话,$refs
一个一个操作起来比较繁琐,vue提供了另外的属性:$parent和$children
来统一选择。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <template> <div>child</div> </template> <script> export default { mounted() { console.log(this.$parent.show()); console.log("Child", this.$children, this.$parent); } }; </script>
<template> <div> parent <Child></Child> <Child></Child> </div> </template> <script> import Child from "./child"; export default { components: { Child }, mounted() { console.log("Parent", this.$children, this.$parent); }, methods: { show() { return "to child data"; } } }; </script>
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
我们在父组件中插入了两个相同的子组件,在子组件中通过$parent
调用了父组件的函数,并在父组件通过$children
获取子组件实例的数组。
我们在Parent中打印出$parent
属性看到是最外层#app的实例。
常见使用场景可以分为三类:
- 父子组件通信: props; $parent/$children; provide/inject; $ref; $attrs/$listeners
- 兄弟组件通信: EventBus; Vuex
- 跨级通信: EventBus; Vuex; provide/inject; $attrs/$listeners