组件通信主要有以下几种方式: props,$emit和$on,vuex,$attrs和$listeners,provide和inject,\$parent,$children与 ref,evenbus以及 $root 按使用场景可以划分为以下三类:

  • 父子组件通信
    • props
    • $emit和$on
    • $parent和$children
    • ref
    • $attrs和$listeners
  • 兄弟组件通信
    • $parent
    • $root
    • eventBus
    • vuex
  • 跨层级通信
    • eventBus
    • provide和inject
    • vuex

# 1、props

  这是父子间组件通信的常见方式,一般用于父组件向子组件传递数据,并且是响应式的。一般情况下子组件不会去直接改变从父组件同过 prop 传过来的数据,如果想改变的话,一般会在子组件 data 中定义一个变量来进行接收:
  注意:这种通信方式是响应式的,一般情况下是用于单向通信的(父向子传递数据),但是如果在通过props特性传的是一个引用类型的数据(如 ObjectArray )时,在子组件修改该引用类型的数据时,也会改变父组件中该props的值。所以这种方式也是可以在兄弟间通信的。如下面的例子,在child1或者child2中修改user 都会触发 user的更新。

//parent
<template>
    <child1 :user="user"/>
    <child2 :user="user"/>
</template>
import child1 from "./child1"
import child2 from "./child2"
export default{
    data(){
        return{
            user:{
                userName:'james'
            }
        }
    },
    components:{
        child1,child2
    },
}

//child1.vue
<template>
    <span>{{userInfo.uaerName}}</span>
    <button @click="change">修改</button>
</template>
export deafult {
    props:{
        user:Object
    }
    data(){
        return{
            userInfo:this.user
        }
    },
    methods:{
        change(){
            this.userInfo.userName="username被修改了"
        }
    }
}

//child2.vue
<template>
    <span>{{userInfo.uaerName}}</span>
</template>
export default{
    props:{
        user:Object
    }
    data(){
        return{
            userInfo:this.user
        }
    }
}

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
53
54
55
56

# 2、 $emit和$on

  这种通信方式主要是 解决子组件向父组件传递数据 的问题。不适合数据隔代传递(跨组件)。

//子组件中
<button @click="send"></button>
...
methods:{
    send(){
        this.$emit("sendMsg","我是子组件的数据")
    }
}
//父组件中
<child @sendMsg="sendMsg"></child>
methods:{
    sendMsg(data){
        console.log(data);//"我是子组件的数据"
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 3、vuex

  这种通信方式属于全局通信方式,一般vuex用在中大型项目中。 主要是有以下几个核心概念:

1、state: 用于保存整个项目中用到的全局变量,是响应式的。与之对应的是mapState函数(当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性)具体的用法可以参考官方文档。Vuex (opens new window)

2、getters: 可以认为是 store 的计算属性,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。 mapGetters: mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。

3、Mutation: 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。
注意: 1、Mutation 必须是同步函数;2、使用常量替代 Mutation 事件类型;3、Mutation 需遵守 Vue 的响应规则。
mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

4、Action: Action 提交的是 mutation,而不是直接变更状态。 Action 可以包含任意异步操作。Action 通过 store.dispatch 方法触发。 在组件中分发 Action:this.$store.dispatch('xxx') 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)。

5、Module Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

# 4、$attrs和$listeners

attrs: 包含了父作用域中不被 props 所识别 (且获取) 的特性绑定。当一个组件没有声明任何 props 时,这里会包含所有父作用域的绑定 ,并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。 inheritAttrstrue 时,表示在该组件上一html元素属性显示那些非 props 属性,为 false 则不显示。

listeners: 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。

//parent
<template >
    <div>
        <child1 :user="userInfo" 
        class="test"
        :userName="userInfo.userName" :age='userInfo.age'>
        </child1>
         <button @click="change">调用子组件方法</button>
    </div>
</template>
<script>
import child1 from "./child1"
export default {
    name:"parent",
    data () {
        return {
            userInfo:{
                userName:'james',
                age:27
            }
        }
    },
    components:{
        child1
    },
    methods: {
        getUserName(){
            return this.userInfo.userName
        },
        change(){
            this.$children[0].change()
        }
    },
}
</script>
//child1
<template >
    <div>
        <span>{{userInfo.userName}}</span>
        <button @click="change">修改</button>
        <button @click="getUserInfo">调用父组件的方法</button>
    </div>
</template>
<script>
export default {
    name:"child1",
    props:{
        user:Object
    },
    data(){
        return{
            userInfo:this.user
        }
    },
    created(){
        console.log(this.$attrs)
        //{age: 27,userName: "james"}
        console.log(this.$listeners)
        //可以直接调用this.$listeners.click()
    },
    methods:{
        change(){
            this.userInfo.userName="userName改变了"
        },
        getUserInfo(){
            const userName=this.$parent.getUserName()
            console.log(userName)
        }
    }
}
</script>
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

# 5、provide和inject

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。 子孙组件通过 inject 注入获取到祖级和父级 provide 的数据。

注意:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

   provide与inject 实现数据响应式的两种方式:

1、provide祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西比如 props,methods 2、使用2.6最新API Vue.observable 优化响应式 provide (推荐)

//parent.vue
<template >
    <div>
        <child1 :user="userInfo" :userName="userInfo.userName"  class="test" :age='userInfo.age' >
        </child1>
        {{userInfo}}
    </div>
</template>
<script>
import child1 from "./child1"
export default {
    name:"parent",
    data () {
        return {
            userInfo:{
                userName:'james',
                age:27
            }
        }
    },
    provide(){
        return {
             user:this.userInfo
        }
    },
    watch:{
        'userInfo.userName':()=>{
            //在子组件中改变该属性,也会被监听到
            console.log("userName改变了")
        }
    },
    components:{
        child1
    },
    methods: {
        getUserName(){
            return this.userInfo.userName
        },
    },
}
</script>
//child1.vue
<template >
    <div>
        <span>{{user.userName}}</span>
        <button @click="change">修改</button>
    </div>
</template>
<script>
export default {
    name:"child1",
    inject:['user'],
    created(){
        console.log(this.user)
    },
    methods:{
       change(){
           this.user.userName="test"
       }
    }
}
</script>
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
53
54
55
56
57
58
59
60
61
62

# 6、$parent,$children与 ref

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。
parent / children:访问父 / 子实例   这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。这两种方法的弊端是,无法在跨级或兄弟间通信。如果是封装通用组件时 parent / children 这种通信手段就显得不足了。

//parent
<template >
    <div>
        <child1 :user="userInfo">
        </child1>
        <button @click="change">调用子组件方法</button>
    </div>
</template>
<script>
import child1 from "./child1"
export default {
    name:"parent",
    data () {
        return {
            userInfo:{
                userName:'james',
            }
        }
    },
    components:{
        child1
    },
    methods: {
        getUserName(){
            return this.userInfo.userName
        },
        change(){
            this.$children[0].change()
        }
    },
}
</script>
//child
<template >
    <div>
        <span>{{userInfo.userName}}</span>
        <button @click="change">修改</button>
        <button @click="getUserInfo">调用父组件的方法</button>
        <!-- <slot name='child1' childData="{test:'name'}"></slot> -->
    </div>
</template>
<script>
export default {
    name:"child1",
    props:{
        user:Object
    },
    data(){
        return{
            userInfo:this.user
        }
    },
    methods:{
        change(){
            this.userInfo.userName="userName改变了"
        },
        getUserInfo(){
            //调用父组件方法
            const userName=this.$parent.getUserName()
            console.log(userName)
        }
    }
}
</script>
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
53
54
55
56
57
58
59
60
61
62
63
64

element-ui在封装通用表单通用组件时,并没有在组件中直接通过 this.$parent、this.$children 来调用父/子组件的方法。为实现组件解耦,做了一层封装。具体封装封装代码如下。

export default {
  methods: {
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root;
      var name = parent.$options.componentName;
      //父组件存在,父组件的componentName跟你传进来的componentName要一致
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName;
        }
      }
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      this.$children.forEach(child => {
        var name = child.$options.componentName;
        if (name === componentName) {
          child.$emit.apply(child, [eventName].concat(params));
        } else {
          //调用自身
          broadcast.apply(child, [componentName, eventName].concat([params]));
        }
      });
    }
  }
};
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

# 7、eventBus

  这种通信方式可以跨组件通讯,经常用于兄弟组件间通讯。这种通讯方式的实现是通过新建一个 vue 实例,然后在需要通信的组件间引入,通过 emit 方法触发事件,通过 on 来监听相应事件来实现通讯的功能。更详细的请参考大神的介绍——vue组件间通信六种方式 (opens new window)

//evenBus.js
import Vue from "vue"
export default new Vue();
1
2
3
Last Updated: 6/6/2024, 9:13:11 AM