Vue组件间通讯遵循单项数据流的原则:父组件→子组件。
如果子组件想要更改父组件的数据,需要通知父组件去修改,本质上还是父组件自己去更改数据。
组件间的通信归纳为以下:
目录
$parent / $children 父组件 ⇋ 子组件 传参
$attrs / $listeners 父组件 → 子组件 传参
$ref 传参(父组件可以在子组件渲染完成后拿到子组件参数)
$bus.$on / $emit 传参 (EventBus各组件相互通信)
Props 父组件 -> 子组件 传参
![]()
页面展示 描述:父组件中子组件<son />绑定了share属性,值为父组件的data:"father_share_data"。同时子组件这边使用props接收属性(参数)share,完成父组件→子组件传参
【父组件.vue 代码】
<template> <div id="father"> <span>[father]</span> <son :share="data" /> </div> </template> <script> import son from "./Son.vue"; export default { data() { return { data: "father_share_data", }; }, components: { son, }, }; </script>
【子组件.vue 代码】
<template> <div id="son"> <span>[son]</span> <div>{{ share }}</div> </div> </template> <script> export default { data() { return { data: 'son_share_data', }; }, props: ["share"], }; </script>
$emit 子组件 -> 父组件 传参
![]()
页面展示 描述:在子组件中监听click事件并用$emit方法触发名为share的自定义事件,传参子组件的data值,当点击div元素后,父组件监听名为share的自定义事件,当触发时父组件的data赋值子组件传过来的参数,也就是就是data : "son_share_data"。
【父组件.vue 代码】
<template> <div id="father"> <div>[father]</div> <div>{{ data }}</div> <son @share="(data) => (this.data = data)" :share="data" /> </div> </template> <script> import son from "./Son.vue"; export default { data() { return { data: "father_share_data", }; }, components: { son, }, }; </script>
【子组件.vue 代码】
<template> <div id="son"> <span>[son]</span> <div @click="$emit('share', data)">{{ share }}</div> </div> </template> <script> export default { data() { return { data: "son_share_data", }; }, props: ["share"], }; </script>
可以使用语法糖 .sync / update:xxx 简化代码
在上面的例子中,父组件向子组件传了share的参数,而子组件通过click触发自定义事件要求父组件修改share参数的值,那么只用写一次share即可;而子组件$emit触发的自定义事件需要改写为'update:[prop]',意为“更新prop属性的自定义事件”(为什么特地指出?因为$emit方法就是用来触发自定义事件的,这个自定义事件的名字本来是开发者随意取的,但是使用了语法糖就限制必须要这么命名)。这样就实现父子组件双向传参。
【父组件.vue 代码】
<template> <div id="father"> <div>[father]</div> <div>{{ data }}</div> <!-- 之前的 --> <!-- <son @share="(data) => (this.data = data)" :share="data" /> --> <!-- 使用sync语法糖 --> <son :share.sync="data" /> </div> </template>
【子组件.vue 代码】
<template> <div id="son"> <span>[son]</span> <!-- 之前的 --> <!-- <div @click="$emit('share', data)">{{ share }}</div> --> <!-- 使用sync语法糖 --> <div @click="$emit('update:share', data)">{{ share }}</div> </div> </template>
还可以使用 v-model / value / input 语法糖
这种方法限制就比较明显。首先父组件只绑定需要向子组件传参的属性,上面的例子中父组件需要向子组件传data : "father_share_data" ,则只绑定这一个参数;子组件接收的参数不能随意命名,必须命名为‘value’。而触发的自定义事件必须命名为'input'。
【父组件.vue 代码】
<template> <div id="father"> <div>[father]</div> <div>{{ data }}</div> <!-- 之前的 --> <!-- <son @share="(data) => (this.data = data)" :share="data" /> --> <!-- 使用sync语法糖 --> <!-- <son :share.sync="data" /> --> <!-- 使用v-model语法糖 --> <son v-model="data" /> </div> </template>
【子组件.vue 代码】
<template> <div id="son"> <span>[son]</span> <!-- 之前的 --> <!-- <div @click="$emit('share', data)">{{ share }}</div> --> <!-- 使用sync语法糖 --> <!-- <div @click="$emit('update:share', data)">{{ share }}</div> --> <!-- 使用v-model语法糖 --> <div @click="$emit('input', data)">{{ value }}</div> </div> </template> <script> export default { data() { return { data: "son_share_data", }; }, props: ["value"], }; </script>
如果开发者就是想使用v-model指令,但不想用‘value’作为属性名,也不想强制使用'input'作为自定义事件的名字。
Vue提供了一个额外的属性model,在子组件里定义这个属性,这个属性值为对象,接收两个属性 prop / event ,这两个值为String类型,即自己定义名字。修改后props里接收的属性要改成自己设定的自定义名称,$emit触发自定义事件改为自己设定的名字。父组件不用改,这样和上面的效果是一样的。
【子组件.vue 代码】
<template> <div id="son"> <span>[son]</span> <!-- 之前的 --> <!-- <div @click="$emit('share', data)">{{ share }}</div> --> <!-- 使用sync语法糖 --> <!-- <div @click="$emit('update:share', data)">{{ share }}</div> --> <!-- 使用v-model语法糖 --> <!-- <div @click="$emit('input', data)">{{ value }}</div> --> <!-- 使用v-model语法糖,但修改成自定义prop和自定义事件名 --> <div @click="$emit('my_event', data)">{{ my_value }}</div> </div> </template> <script> export default { data() { return { data: "son_share_data", }; }, props: ["my_value"], model: { prop: "my_value", event: "my_event", }, }; </script>
$parent / $children 父组件 ⇋ 子组件 传参
描述:通过$parent、$children 组件间可以直接访问到父子组件的实例对象。需要注意的是,页面组件的渲染顺序是父组件→子组件,父组件在创建时拿不到子组件的数据,可以用mounted钩子函数在dom挂载后,父组件调用 this.$children[xxx] 拿到子组件实例上的任何数据;而子组件创建时能拿到父组件的数据,可以直接用插入符号{{ this.$parent[xxx] }}直接拿到父组件的所有数据,甚至直接修改父组件的数据。虽然简单粗暴,但是打破单项数据流的原则,当出现复用组件且多组件依赖的情况,不慎使用会造成数据混乱。
![]()
页面展示 【父组件.vue 代码】
<template> <div id="father"> <div>[father]</div> <div>{{ sonData || data }}</div> <son /> </div> </template> <script> import son from "./Son.vue"; export default { data() { return { data: "father_share_data", sonData: null, }; }, components: { son, }, mounted() { this.sonData = this.$children[0].data; }, }; </script>
【子组件.vue 代码】
<template> <div id="son"> <span>[son]</span> <div>{{ $parent.data }}</div> </div> </template> <script> export default { data() { return { data: "son_share_data", }; }, }; </script>
$attrs / $listeners 父组件 → 子组件 传参
$attrs 用来传属性,$listeners 用来传函数
描述:
先说 $attrs 。只要是在父组件中给子组件传参,那么这些参数都会收集到子组件实例上的 $attr 属性上,形象的说这就是一种不想在子组件中写props来接收参数的偷懒方法,对比props接收参数来说,它少了数据验证、默认值和要求必须传参等这些属于props选项所带的功能。如果开发者只想传参数不想别的,那么这招偷懒还是很好用的。
再来 $listeners 。只要是在父组件中的子组件上写的事件监听,子组件都可以通过自身的$listeners 访问到父组件的方法,并且能调用直接触发回调函数,需要注意的是触发回调函数的this指向父组件实例,相当于子组件命令父组件执行函数,并没有子组件→父组件传参。
使用 $attrs 、 $listeners 的好处是可以复用地向下级组件传参,用法为在要传参的子组件上写 v-bind='$attrs' / v-on='$listeners' ,就可以实现“较远距离”传参。
![]()
页面展示
通过点击 grandson 组件的div,改变了 father 组件的data值,son 和 grandson 都使用 father 传的 :data = 'data' 值,因而都发生了改变。
【综合代码如下】
```父组件.vue <template> <div id="father"> <div>[father]</div> <div>{{ data }}</div> <son :data="data" @click=" () => { data = this.handle; } " /> </div> </template> <script> import son from "./Son.vue"; export default { data() { return { data: "father_share_data", handle: "handleClick", }; }, components: { son, }, }; </script> ``` ```子组件.vue <template> <div id="son"> <span>[son]</span> <div>{{ $attrs.data }}</div> <grandson v-bind="$attrs" v-on="$listeners" /> </div> </template> <script> import grandson from "./Grandson.vue"; export default { components: { grandson, }, }; </script> ``` ```孙组件.vue <template> <div id="grandson"> <span>[grandson]</span> <div v-on="$listeners">{{ $attrs.data }}</div> </div> </template> ```
$ref 传参(父组件可以在子组件渲染完成后拿到子组件参数)
描述:ref 本义为 reference (引用)的缩写,本来作用就是创建一个引用的,比如在vm实例中获取dom元素是比较麻烦的,要不监听事件在回调函数中拿到event,再event.target拿到dom元素,或者用Vue.directive定义一个自定义事件,回调函数中第一个参数接收一个el参数(也就是绑定dom元素)可以获取到。总之比较麻烦,如果使用 ref 属性就很简单,只要在想要获取dom元素的标签上写上 ref=[自定义名称] ,通过访问该vm实例上的 $refs.xxx (自定义名称),就能拿到dom元素。
![]()
页面展示 <template> <input type="text" value="123" ref="input" /> </template> <script> export default { mounted() { console.log(this.$refs.input.value); //123 }, }; </script>
同理可以使用在组件上,通过此法可以拿到子组件上的数据,本质还是先拿到子组件的vm实例对象,然后再访问实例对象上的属性拿到数据。
```父组件.vue <template> <div id="father"> <div>[father]</div> <son ref="son" /> </div> </template> <script> import son from "./Son.vue"; export default { data() { return { data: "father_share_data", }; }, components: { son, }, mounted() { console.log(this.$refs.son.data); //"son_share_data" }, }; </script> ``` ```子组件.vue <template> <div id="son"> <span>[son]</span> <div>{{ data }}</div> </div> </template> <script> export default { data() { return { data: "son_share_data", }; }, }; </script> ```
$root 传参 (根组件→子孙组件传参)
描述:即 main.js 中的new Vue(options) 根实例上的data上存储参数,此后每个vm实例对象通过自身的 $root 属性拿到根组件上的data属性。需要注意的是:其他参数都是英文单词复数,$root就是英文单数。
![]()
页面展示
```main.js import Vue from 'vue' import App from './App.vue' new Vue({ render: h => h(App), data: { data: "root_share_data", }, }).$mount('#app') ``` ```father.vue <template> <div id="father"> <div>[father]</div> <div>{{ $root.data }}</div> </div> </template> ```
$bus.$on / $emit 传参 (EventBus各组件相互通信)
描述:
①首先创建事件总线对象 $bus 。在 main.js 中给Vue构造函数原型上创一个一个名为 $bus 的Vue实例对象 Vue.prototype.$bus = new Vue();
②定义一个自定义事件。在任意一个组件实例里,使用 this.$bus.$on( 'name' , callback_fn) 指令,第一个参数接收一个String类型的自定义名字,第二个参数定义一个回调函数;
③触发自定义事件。在任意一个组件实例里,使用 this.$bus.$emit( 'name' , arg1 , arg2 ...) 指令,第一个参数接收一个String类型的需要触发的自定义名字,之后参数为给回调函数传的实参。
※需要注意的是:定义一个自定义事件一定要先于触发自定义事件。比如在父组件中 mounted钩子函数里定义一个自定义事件 'my' ,在子组件中 mounted 触发自定义事件 'my' 。会遇到触发不好使的情况。因为渲染vm的顺序是 ...父组件created → ...子组件created → ... 子组件mounted ... 父组件mounted 。看似写在父组件定义自定义事件在前,实际按渲染顺序以后把“先定义再触发的顺序搞反了”。这是可以写异步函数坚决,即调用组件实例上的 this.$nextTick(callback_fn) 方法实现异步执行。
```父组件.vue <template> <div id="father"> <div>[father]</div> <son /> </div> </template> <script> import son from "./Son.vue"; export default { components: { son, }, mounted() { //定义一个自定义事件 this.$bus.$on("my", (data) => { console.log(data); }); }, }; </script> ``` ```子组件.vue <template> <div id="son"> <span>[son]</span> </div> </template> <script> export default { data() { return { data: "son_share_data", }; }, created() { //触发自定义事件 this.$nextTick(() => { this.$bus.$emit("my",this.data); //"son_share_data" }); }, }; </script> ```
Provide & Inject 传参 (祖先模块→子孙模块)
描述:在祖先组件里使用 provide 指令,写成函数的形式,要求返回一个对象,key作为传参的名字,value作为传参的值;在子孙模块中使用 inject 指令,值为数组,数组的成员为一个个String类型的属性名。
![]()
页面展示 ```father.vue <template> <div id="father"> <div>[father]</div> <son /> </div> </template> <script> import son from "./Son.vue"; export default { components: { son, }, //使用provide指令传参 provide() { return { father: "father.vue", }; }, }; </script> ``` ```son.vue <template> <div id="son"> <span>[son]</span> <grandson /> </div> </template> <script> import grandson from "./Grandson.vue"; export default { //使用provide指令传参 provide() { return { son: "son.vue", }; }, components: { grandson, }, }; </script> ``` ```grandson.vue <template> <div id="grandson"> <span>[grandson]</span> <div>{{ father + " " + son }}</div> </div> </template> <script> export default { //使用inject指令接收参数 inject: ["father", "son"], }; </script> ```
类比来说 provide/inject 和 $attrs 和 props 传参的形式类似,本质都是父组件→子孙组件。但是优劣势各有不同,以下是个人总结:
1、provide/inject:适用于跨至少一个关系的祖先元素传参给子孙元素(father和grandson的关系)。代码复杂度高,而且全写在script标签里,不易一眼就看出参数的指向。但对远距离传参不要太好用,传就provide,收就inject,很容易管理。
2、$attrs:适用于父组件给子组件传参,仅在乎把值传过去就行。代码复杂度低,只要在父组件中子组件标签的行间写上传参内容,子组件实例对象就能通过this.$attrs直接拿到值,比较方便。
3、props: 适用于父子组件传参,有参数类型、是否必须传值、默认值设置、自定义验证规则等要求的,以及需要使用双向数据绑定在父子组件通信的情况。代码复杂度适中,传参写在组件标签行间,接收使用 props 指令,是最常用的组件通信传参方式了。
暂时写这么多...