Vue核心应用(五)— 组件间的通讯

Vue CLI 快速原型开发

安装工具

  • npm install @vue/cli@3 -g
  • npm install @vue/cli-service-global@3 -g

启动 vue serve

示例

1)son1改parent中的钱

入口文件 main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from 'vue';
import App from './App';

// 当前文件的作用 主要:加载全局样式 全局组件 全局指令

//1.入口文件 webpack会根据这个入口文件进行打包(这里不需要额外安装webpack,上面的包里已集成)
//2.默认会渲染App.vue组件
// 创建一个vue实例
new Vue({
el: '#app',
render:h=>h(App) // 用h(createElement)
})

// vue serve 启动 会默认去找当前文件夹下的main.js 没有就找App.vue

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--根实例 根组件-->
<template>
<div>
<Parent @say="say"></Parent>
</div>
</template>
<script>
// 组件使用的三部: 声明 + 注册 + 使用
import Parent from './components/parent';
export default{
components:{
Parent
},
methods:{
say(){
console.log('say');
}
}
}
</script>

组件(components)
/components/parent.vue

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>
父亲有{{mny}}
<!-- 这里需要给子组件绑定一个自定义事件 -->
<!-- son1.$on('changeItem', change) 'changeItem'是属性名,后面是值,绑定的是一个函数, = 号右边的内容都是当前组件的内容-->
<!-- 事件绑完之后在这个实例上有个属性 vm._event 订阅 原理-->
<Son1 :mny="mny" @changeItem="change"></Son1>
</div>
</template>

<script>
import Son1 from './son1';
export default {
components:{
Son1
},
data(){
return{
mny: 100
}
},
methods:{
change(newValue){
this.mny = newValue;
}
}
}
</script>

/components/son1.vue
单向数据流 不能子组件直接修改父组件的数据 这里指mny 可以子组件点击让父组件修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
son1 {{mny}}
<button @click="changeMoney()">修改钱</button>
</div>
</template>
<script>
export default{
props:{
mny:{
type: Number // 校验类型
}
},
methods:{
changeMoney(){
// 这里面的这个this 就相当于 son1.$on('changeItem', change) 里面的son1
this.$emit('changeItem',200);
console.log(this._events); // 这里能够打印出发布订阅绑定的事件
// {changeItem: Array(1)} 如下图
}
}
}
</script>

2) grandson1改parent中的钱

  • 孙=>子=>父 三层触发
  • .$parent.$parent.$parent 多级

/components/son1.vue

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>
son1 {{mny}}
<button @click="changeMoney()">修改钱</button>
<!-- 给Grandson1绑定事件 -->
<Grandson1 :mny="mny" @changeGrand="grandChange"></Grandson1>
</div>
</template>

<script>
import Grandson1 from './grandson1';
export default{
components:{
Grandson1
},
props:{
mny:{
type: Number // 校验类型
}
},
methods:{
changeMoney(){
// 这里面的这个this 就相当于 son1.$on('changeItem', change) 里面的son1
this.$emit('changeItem',200);
console.log(this._events); // 这里能够打印出发布订阅绑定的事件
},
grandChange(newValue){
this.$emit('changeItem',newValue);
}
}
}
</script>

/components/grandson1.vue

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>
grandson1
{{mny}}
<button @click="changeMoney">孙子组件改变钱</button>
</div>
</template>
<script>
export default{
props:{
mny:{
type: Number
}
},
methods:{
changeMoney(){
// 触发自己的事件
this.$emit('changeGrand', 300); // 孙=>子=>父 三层触发

// this.$parent.$emit('changeItem', 400); // 直接触发父组件绑定的事件,当前父组件是另一个父组件的子组件,触发了绑定的该父组件的changeItem事件
}
}
}
</script>
  • .$dispatch 自动向上查找

/components/grandson1.vue

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
<template>
<div>
grandson1
{{mny}}
<button @click="changeMoney">孙子组件改变钱</button>
</div>
</template>
<script>
export default{
props:{
mny:{
type: Number
}
},
methods:{
changeMoney(){
// 触发自己的事件
// this.$emit('changeGrand', 300); // 孙=>子=>父 三层触发
// this.$parent.$emit('changeItem', 400); // 直接触发父组件绑定的事件,当前父组件是另一个父组件的子组件,触发了绑定的该父组件的changeItem事件

this.$dispatch('changeItem', 500);
// 自动向上查找他的父亲,有changeItem的话就触发 在main里写一个这个方法
}
}
}
</script>

main.js

1
2
3
4
5
6
7
Vue.prototype.$dispatch = function(eventName,value){
let parent = this.$parent; // 先找第一层的parent
while(parent){
parent.$emit(eventName,value); // 触发方法
parent = parent.$parent; // 接着向上找
}
}

3) 往下

注意:一定要先订阅,才能触发

触发所有子组件带有 eat 方法
main.js中写 $broadcast方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 验证这个方法 在App.vue中加个say,底下子组件调用 
Vue.prototype.$broadcast = function(eventName) {
let children = this.$children;
function broad(children){
children.forEach(child => { // 如果自己的儿子下面还有儿子 继续查找
child.$emit(eventName); // 触发当前儿子上的对应事件
if(child.$children){
broad(child.$children)
}
});
}
broad(children); // 先找自己的儿子
}

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--根实例 根组件-->
<template>
<div>
<Parent></Parent>
<button @click="broadcast">触发所有子组件带有 eat 的方法</button>
</div>
</template>
<script>
import Parent from './components/parent';
export default{
components:{
Parent
},
methods:{
broadcast(){
console.log("App-this:", this);
this.$broadcast('eat');
}
}
}
</script>

parent.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<!-- 这里需要给子组件绑定一个自定义事件 -->
<!-- son1.$on('changeItem', change) 'changeItem'是属性名,后面是值,绑定的是一个函数 =号右边的内容都是当前组件的内容-->
<!-- 事件绑完之后在这个实例上有个属性 vm._event 订阅 原理-->
<Son1 @eat="eatFun" ></Son1>
<!-- 注意: 这里的 @eat="eatFun" 就是订阅的过程,如果没有订阅 则不会触发。************************************************** -->
</div>
</template>

<script>
import Son1 from './son1';
export default {
components:{
Son1
},
methods:{
eatFun(){
console.log('parent-eat~')
}
}
}
</script>

son1.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<!-- 给Grandson1绑定事件 -->
<Grandson1 @eat="eat"></Grandson1>
</div>
</template>

<script>
import Grandson1 from './grandson1';
export default{
components:{
Grandson1
},
methods:{
eat(){
console.log('son-eat~~');
}
}
}
</script>

点击App.vue中的 ,打印: parent-eat~    son-eat~~

4) 子组件 => 父组件(多种写法)

> 4.1

parent.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<Son2 :count="count" @update:count="newValue=>count=newValue"></Son2>
</div>
</template>
<script>
import Son2 from './son2';
export default {
components:{
Son2
},
data(){
return{
count: 1,
}
}
}
</script>

son2.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
son2
count: {{count}}
<button @click="changeCount">点我改变count</button>
</div>
</template>
<script>
export default {
props:{
count:{
type: Number // 类型检测
}
},
methods:{
changeCount(){
this.$emit("update:count",2); //注意这里触发的写法 ****************************************
}
}
}
</script>

点击子组件 ,修改父组件 count的值 1 => 2

> 4.2

parent.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<!-- <Son2 :count="count" @update:count="newValue=>count=newValue"></Son2> -->
<!--1.下面是上一行的语法糖 写成 :count.sync 那么子组件中$emit触发的事件必须是 update:变量名 这里是count-->
<Son2 :count.sync="count"></Son2>
</div>
</template>

<script>
import Son2 from './son2';
export default {
components:{
Son2
},
data(){
return{
count: 1,
}
}
}
</script>

son2.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
son2
count: {{count}}
<button @click="changeCount">点我改变count</button>
</div>
</template>
<script>
export default {
props:{
count:{
type: Number // 类型检测
}
},
methods:{
changeCount(){
this.$emit("update:count",2);
}
}
}
</script>

点击子组件 ,修改父组件 count的值 1 => 2

> 4.3

parent.vue

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>
<!-- <Son2 :count="count" @update:count="newValue=>count=newValue"></Son2> -->
<!--1.上一行的语法糖 写成 :count.sync 那么子组件中$emit触发的事件必须是 update:变量名 这里是count-->
<!-- <Son2 :count.sync="count"></Son2> -->

<!--这里传过去的值不是名字不是count,而是value,对应的子组件也需要改下,触发的方法是 input-->
<Son2 :value="count" @input="newValue=>count=newValue"></Son2>
</div>
</template>

<script>
import Son2 from './son2';
export default {
components:{
Son2
},
data(){
return{
count: 1,
}
}
}
</script>

son.vue

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>
<!-- son2
count: {{count}}
<button @click="changeCount">点我改变count</button> -->
<br>
"value":
{{value}}
<button @click="changeValue">点我改变value</button>
</div>
</template>

<script>
export default {
props:{
count:{
type: Number // 类型检测
},
value:{
type: Number
}
},
methods:{
// changeCount(){
// this.$emit("update:count",2);
// },
changeValue(){
this.$emit('input',3);
}
}
}
</script>

点击子组件 ,修改父组件 count的值 1 => 3

> 4.4

parent.vue

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
<template>
<div>
<!-- <Son2 :count="count" @update:count="newValue=>count=newValue"></Son2> -->
<!--1.上一行的语法糖 写成 :count.sync 那么子组件中$emit触发的事件必须是 update:变量名 这里是count-->
<!-- <Son2 :count.sync="count"></Son2> -->

<!--这里传过去的值不是名字不是count,而是value,对应的子组件也需要改下,触发的方法是 input-->
<!-- <Son2 :value="count" @input="newValue=>count=newValue"></Son2> -->
<!-- 2. 这个写法是上面写法的替代品,默认组件内部需要触发 input-->
<Son2 v-model="count"></Son2>
<!-- v-model局限 只能传递一个属性,如果只有一个可以使用v-model,多个依然需要使用.sync -->
</div>
</template>

<script>
import Son2 from './son2';
export default {
components:{
Son2
},
data(){
return{
count: 1,
}
}
}
</script>

son.vue

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>
<!-- son2
count: {{count}}
<button @click="changeCount">点我改变count</button> -->
<br>
"value":
{{value}}
<button @click="changeValue">点我改变value</button>
</div>
</template>

<script>
export default {
props:{
count:{
type: Number // 类型检测
},
value:{
type: Number
}
},
methods:{
// changeCount(){
// this.$emit("update:count",2);
// },
changeValue(){
this.$emit('input',3);
}
}
}
</script>

5) 传递多个属性

将多个属性 mny、count都传给son2

> 5.1 $attrs 属性的集合

parent.vue

1
<Son2 :mny="mny" :count="count"></Son2>

son2.vue

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
son2
<!-- 属性的集合 -->
{{$attrs}}
</div>
</template>
<script>
export default {
}
</script>

如下图:

> 5.2 props + $attrs

parent.vue

1
<Son2 :mny="mny" :count="count"></Son2>

son2.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>
son2
<!-- //属性的集合 -->
{{$attrs}}
</div>
</template>

<script>
// 如果组件中使用了 props 就会将 attrs 从当前的 $attrs 移除掉
export default {
props:{
mny:{} // 从$attrs中移除mny属性
},
}
</script>

如下图:

> 5.3 inheritAttrs

parent.vue

1
<Son2 :mny="mny" :count="count"></Son2>

son2.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
son2
<!-- //属性的集合 -->
{{$attrs}}
</div>
</template>

<script>
// 如果组件中使用了 props 就会将 attrs 从当前的 $attrs 移除掉
export default {
// props:{
// mny:{} // 从$attrs中移除mny属性
// },
// 不用props写 也可以用下面这个属性
inheritAttrs: false,
}
</script>

如下图:

官方文档:
inheritAttrs: 默认情况下父作用域的不被认作 props 的特性绑定 (attribute bindings) 将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例属性 $attrs 可以让这些特性生效,且可以通过 v-bind 显性的绑定到非根元素上。 inheritAttrs默认为true。

> 5.4 $attrs 应用

父传递给子 => 儿子 有属性用不到 => 孙子
parent.vue

1
<Son2 :mny="mny" :count="count"></Son2>

son2.vue

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>
son2
<!-- 属性的集合 -->
{{$attrs.mny}}
<Grandson2 v-bind="$attrs"></Grandson2>
<!-- 可以理解为把当前组件的所有属性都传递下去 如果这里line19-23注释打开,即props里面有用到属性,那么mny属性就不会往下传-->

</div>
</template>

<script>
import Grandson2 from './grandson2';
export default {
components:{
Grandson2
},
inheritAttrs: false,
// props:{
// mny:{
// type:Number
// }
// },
}
</script>

如下图:

6) 传递事件

要给组件传递一个show方法 跨组件传递 父 => 孙

> 6.1 .native

parent.vue

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>
<!-- 传递事件 要给组件传递一个show方法 跨组件传递 父 => 孙 -->
<!-- 如下这样写,给Son2绑定show方法,点击Son2是不会有click事件的,因为这是自定义事件 这个@click和上面的 @input一样,名字和原生事件相同 想要变成原生事件可以如下加上.navive-->
<!-- 这时点击son2组件没有反应 *********************************-->
<Son2 :mny="mny" :count="count" @click="show"></Son2>

<!-- .native 就是给组件的最外层div元素上绑定事件-->
<!-- 这时点击son2组件最外层就会触发show事件 ***********************-->
<Son2 :mny="mny" :count="count" @click.native="show"></Son2>
</div>
</template>

<script>
import Son2 from './son2';
export default {
components:{
Son2
},
data(){
return{
mny: 100,
count: 1,
}
},
methods:{
show(){
console.log('show~~');
},
}
}
</script>
> 6.2 $listeners

现在希望点击 grandson2 中的 button 时才触发 parent 中的 show 方法
在son2中添加$listeners
parnet.vue

1
2
<!-- 现在希望点击grandson中的button时才触发show方法   在son2中添加$listeners  这里的 @click @mouseup 都不是原生事件 同名而已-->
<Son2 :mny="mny" :count="count" @click="show" @mouseup="show"></Son2>

son2.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<!-- 传递事件 可以在mounted中打印 v-bind 将属性全部向下传递 方法绑定v-on 将方法全部向下传递 -->
<Grandson2 v-bind="$attrs" v-on="$listeners"></Grandson2>
</div>
</template>

<script>
import Grandson2 from './grandson2';
export default {
mounted(){
console.log("listeners:",this.$listeners); // 如下图
},
components:{
Grandson2
},
}
</script>

grandson2.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div>
grandson2
{{$attrs}}
<!-- 这里的 @click 和 $mouseup 是原生的-->
<button @click="$listeners.click">点我click</button>
<button @mouseup="$listeners.mouseup">点我mouseup</button>
</div>
</template>
<script>
export default {
}
</script>

如上,点击 grandson2.vue 中的 button 触发的是 parent.vue 中的say方法

> 6.3 provide && inject

grandson2.vue 触发 App.vue 中的方法

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
import Parent from './components/parent';
export default{
provide(){
return { vm: this} // 暴露全局方法
},
components:{
Parent
},
methods:{
say(){
console.log('say-provide');
}
}
}
</script>

grandson2.vue

1
2
3
4
5
6
7
8
9
<script>
export default {
inject:['vm'], // 注入 会向上查找 如果重名,找到就停止。
mounted(){
console.log(this.vm); // 这个vm就是App.vue组件的实例
this.vm.say(); // 可以直接触发父级方法
}
}
</script>
> 6.4 ref && $ref

parent.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>
<!-- ref示例 -->
<Son2 ref="son2" :mny="mny" :count="count" @click="show" @mouseup="show"></Son2>
</div>
</template>
<script>
import Son2 from './son2';
export default {
// refs 示例
mounted(){
this.$refs.son2.alertLayer(); // 直接在父组件中拿到组件的实例,并且ref不要重名,只有v-for才会出现数组的情况
},
components:{
Son2
},
}
</script>

son2.vue

1
2
3
4
5
6
7
8
9
<script>
export default {
methods:{
alertLayer(){// ref示例
alert("ref示例");
}
}
}
</script>

如上,页面弹出 alert(“ref示例”);

和上述 provide && inject 相反,provide && inject是父组件将实例暴露出来供子组件调用,ref是父组件去调用子组件的实例。

> 6.5 $bus

main.js

1
2
3
4
// 创建一个全局的发布订阅 偶尔用一次还可以,使用起来比较混乱
Vue.prototype.$bus = new Vue(); // 创建一个实例 把它放到$bus上
// 创建的这个vue实例上有 $on $emit ,这样就可以去任何的一个组件中绑定事件
// 这样就可以全局调用 $bus

parent.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
import Son2 from './son2';
export default {
mounted(){
this.$bus.$on("changeBus",function(){ // this.$bus 是通过全局的实例去绑定事件 (订阅)
console.log("change bus绑定事件")
})
},
components:{
Son2
},
}
</script>

grandson2.vue

1
2
3
4
5
6
7
8
9
10
11
12
<script>
export default {
mounted(){
this.$bus.$emit("changeBus"); //这样是不会执行的,多级组件的挂载顺序
// 可以
this.$nextTick(()=>{
this.$bus.$emit("changeBus");
})
// 这样就会执行 console.log 打印出 change bus绑定事件
}
}
</script>

总结

  • 父传递给子数据 props emit (这个用的多些)
  • $parent $children
  • $attrs $listeners
  • provide inject 和 context很像 (可以在父组件中声明一个公共数据),在子组件中可以注入原理 (特点:比较混乱,名称问题 他不会在业务代码中使用) 组件库 多级通信时为了方便可以使用provide
  • ref 能获取真实dom元素,如果放到组件上 代表的是 当前组件的实例 ,父组件中可以直接获取子组件的方法或者数据 示例 6.4
  • eventbus (像 $parent,$children 中,比如 a.$on() 必须 a.$emit() 来触发, b.$emit() 不行) 绑定 $on 只能通过绑定 $on 的那个组件来触发, (这里 eventbus 使用混乱,不管在哪里绑定都能触发 $bus )

vue 初学者用 props + emit 就够用了