vue组件间通信

223 阅读3分钟

写在前面

3种通信模式:父子组件通信、兄弟组件通信、隔代通信

7种组件通信方式

  • props && $emit

  • parent && root && $children

  • $attrs

  • ref

  • provide && inject

  • 事件总线EventBus

  • vuex

1. props && $emit

<!-- 示例1:-->
<body>
    <div id="app">
        <!-- myCpn自定义组件写成my-cpn  cpnList是子组件props中声明的属性写成cpn-list,接受父组件传递的属性-->
        <!-- v-bind:cpn-list="list" 简写 :cpn-list="list" -->
        <!-- cpn-list是子组件props中定义的属性cpnList,用于接收父组件传递来的list -->
        <my-cpn :cpn-list="list" :cpn-pers-info="persInfo"></my-cpn>
    </div>
    <script>
        Vue.component('myCpn', {
            // 在子组件的模板中使用子组件props中的属性
            template: `<div>
                        <h1>我是子组件myCpn</h1>
                        <h1>接收到的数据:{{cpnList}}</h1>
                        <h1>接收到的数据:{{cpnPersInfo}}</h1>
                    </div>`,
            props: ['cpnList', 'cpnPersInfo']
        });
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    list: ['数码', '家电', '旅行'],
                    persInfo: {
                        name: 'Bob',
                        age: 20
                    },
                }
            }
        });
    </script>
</body>
<!-- 示例1:-->
<body>
    <!-- 子组件模板 -->
    <template id="son">
        <div>
            <!-- 1. 子组件通过点击事件触发事件函数 -->
            <button v-for="item of categories" @click="btnClick(item)">{{item.name}}</button>
        </div>
    </template>
  
    <!-- 父组件模板 -->
    <div id="app">
        <!-- 3. 父组件通过监听子组件的自定义事件item-click进行处理,注意自定义的事件的函数没有event对象,但是这里默认会将子组件的数据item带过去 -->
        <cpn-button @item-click="cpnClick"></cpn-button>
    </div>
  
    <script>
        // 子组件
        Vue.component('cpnButton', {
            data() {
                return {
                    categories: [
                        { id: '1', name: '家电' },
                        { id: '2', name: '科技' },
                        { id: '3', name: '户外' },
                    ],
                }
            },
            template: '#son',
            methods: {
                btnClick(item) {
                        // 2. 子组件将点击获取到的数据通过 this.$emit(自定义事件名,传递给父组件的数据)发射自定义事件;自定义事件名要小写短横线连接
                    this.$emit('item-click', item)
                }
            }
        })

        // 父组件实例
        new Vue({
            el: '#app',
            methods: {
                // 4. 父组件监听的自定义事件函数接收到子组件传递来的item数据 
                cpnClick(item) {
                    console.log("---", item)
                }
            }
        })
    </script>
</body>

2. $parent && $root && $children

  • this.$parent:可以访问到父组件,但是不提倡这样做,这样会破坏子组件的复用性,比如在子组件中修改父组件A的name属性,但是其他B的组件引用这个子组件,B组件中并没有name属性;会导致没有解耦

  • this.$root: 会直接访问到根实例

  • this.$children:是一个包含所有直接子组件对象的数组;比如上面例子中的A、B、C;在A组件中this.children数组只有一个元素就是B组件,但是B组件的this.children数组只有一个元素就是B组件,但是B组件的this.children包含一个C组件;

// 在A组件中使用下面这个可以访问到C组件的msgC属性
this.$children[0].$children[0].msgC
<!-- this.$children示例 -->
<body>
  <div id="app">
      <cpn></cpn>
      <button @click="btn">按钮</button>
  </div>
  <template id="cpn">
      <div>
          <h1>{{message}}</h1>
      </div>
  </template>
  <script>
      var vm = new Vue({
          el: "#app",
          data: {
              message: "我是父组件app",
          },
          methods: {
              btn() {
                  console.log(this.$children[0].message)
                  this.$children[0].show();
              }
          },
          components: {
              cpn: {
                  template: '#cpn',
                  data() {
                      return {
                          message: "我是子组件cpn",
                      }
                  },
                  methods: {
                      show() {
                          console.log(this.message);
                      }
                  }

              }
          }
      });
  </script>
</body>

3. $attrs组件间通信

vm.$attrs: 是一个对象,用于接受父组件传递的所有的属性。

一般我们接受父组件传递的属性都是在子组件中定义props选项接受父组件传递的值。

attrs会包含所有父作用域的绑定(classstyle除外),并且可以通过vbind="attrs会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="attrs" 传入内部组件

试想有A,B,C祖孙3代组件,我们想从A组件传递属性a到C组件,但是如果采用props的方式,就会导致在中间层B组件中定义props用于接受A组件传值a,然后再通过B组件props接受的a,传递到C组件中,但是B组件并没有用a,却还是需要定义props属性接受;如果组件嵌套层数很多的情况,中间组件就会定义大量的props导致浪费。

attrs可以很方便的做到属性透传,使用起来也比较简单,避免了中间组件多写props 的痛苦

<!-- 示例1:-->
<!-- A.vue -->
<template>
  <div>
    我是A组件
    <B :msg-a="msgA" :list-a="listA"></B>
  </div>
</template>
<script>
import B from "./B";
export default {
  data () {
    return {
      msgA: 'hello msgA',
      listA: ['五毒神掌', '黯然销魂掌', '降龙十八掌']
    }
  },
  components: {
    B
  }
}
</script>


<!-- B.vue -->
<template>
  <div>
    我是B组件
    <C v-bind="$attrs" :msg-b="msgB"></C>
  </div>
</template>
<script>
import C from "./C";
export default {
  data () {
    return {
      msgB: 'hello msgB'
    }
  },
  components: {
    C
  }
}
</script>

<!-- C.vue -->
<template>
  <div>我是C组件{{ msgA }} -- {{ msgB }} -- {{ listA }}</div>
</template>
<script>
export default {
  props: ['msgA', 'msgB', 'listA']
}
</script>

// main.js
import Vue from "vue";
import A from "./components/A.vue";
new Vue({
  render: h => h(A)
}).$mount("#app");

image.png

<!-- 示例2:注意点 -->
<!-- B.vue -->
<template>
  <div>
    我是B组件{{ listA }}-- {{ msgA }}
    <C v-bind="$attrs" :msg-b="msgB"></C>
  </div>
</template>
<script>
import C from "./C";
export default {
  data () {
    return {
      msgB: 'hello msgB'
    }
  },
  props: ['msgA', 'listA'],
  components: {
    C
  }
}
</script>
<!-- 示例2总结:A透传给C的属性在$attrs里面,在透传的过程中B从$attrs中取走了A透传的listA和msgA,那么这两个listA和msgA属性就会从 $attrs中被剔除,所以C组件就无法从$attrs获取到A透传的属性了-->

image.png

image.png

4. ref

定义:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

作用:简单说就是给子组件上绑定ref,则父组件就可以通过this.$ref.子组件绑定的ref的值获取到子组件的实例.

$ref结构:默认是空对象,当在子组件上绑定ref属性时,this.$refs的结构就变成{ 绑定的ref属性: 对应的子组件 }

注意点:当ref获取组件时,组件并不是DOM元素,所以无法直接访问dom元素的属性;需要借助组件的$el获取到组件的dom元素

// this获取组件之后再通过组件的$el获取到组件的dom元素,进而访问dom属性
this.$ref.xxx.$el.style.color = red

4.1 通过ref直接访问dom元素

// 示例1:ref访问dom元素
<body>
    <div id="app">
      	// 给p元素绑定ref属性,则父组件vm实例可以通过vm.$ref.pref引用到p元素进而操作p元素
        <p ref="pref" attr="ppp">我是p元素</p>
        <div>
            {{ getElement() }}
        </div>
    </div>
    <script>
        var vm = new Vue({
            el: '#app',
            methods: {
                getElement() {
                    // this.$refs.pref指向p元素
                    // vm.$refs.pref.getAttribute('attr')可以获取到p元素上的attr属性值
                    return this.$refs.pref
                }
            }
        })
    </script>
</body>

4.2 通过ref直接访问子组件实例

// 示例2:通过ref访问子组件实例
<div id="app">
  <cpn ref="cpnRef"></cpn>
  <button @click="getcpn">btn</button>
  <p>{{ msg }}</p>
</div>

<script>
  var vm = new Vue({
    el: '#app',
    data() {
      return {
        msg: ''
      }
    },
    components: {
      cpn: {
        template: '<div>我是cpn组件</div>',
        data() {
          return {
            cpnMsg: "我是cpn组件的cpnMsg"
          }
        }
      }
    },
    methods: {
      getcpn() {
        // this.$refs.cpnRef指向cpn组件实例;可以简单调用cpn组件的属性或者方法
        // this.$refs.cpnRef.cpnMsg 获取到cpn子组件的属性值
        this.msg = this.$refs.cpnRef.cpnMsg
      }
    }

  })
</script>

5. provide && inject

核心思想:相当于react的context。父组件在provider中暴露属性、方法、实例等,子或者孙...可以在inject中获取到父组件暴露出来的数据(属性、方法、实例等)

缺点

  1. 不适合兄弟组件通信,或者两个不相干的组件通信
  2. 只能父组件给后代暴露,供后代使用;不能后代向父组件暴露

优点:适合父组件和嵌套层次深的孙孙...组件间的通信。

注意点:1. provider暴露的名称,必须和inject引入的名称一直;2. provider暴露方法时,必须在父组件中提前绑定好this

// 经典案例:el-form和el-form-item之间的通信,使用的就是provide && inject
<el-form  label-width="80px" :model="formData">
  <el-form-item label="姓名">
    <el-input v-model="formData.name"></el-input>
  </el-form-item>
  <el-form-item label="年龄">
    <el-input v-model="formData.age"></el-input>
  </el-form-item>
</el-form>

基本用法:

// 父组件
<template>
  <div class="father" >
     <div>子组件对我说:{{  sonMes  }}</div>
     <div>孙组件对我说:{{  grandSonMes  }}</div>
     <son />
  </div>
</template>
<script>
import son from './son'
export default {
   name:'father',
   components:{
       son /* 子组件 */
   },
   provide(){
       return {
           /* 将自己暴露给子孙组件 ,这里声明的名称要于子组件引进的名称保持一致 */
           father:this
       }
   },
   data(){
       return {
          grandSonMes:'', /* 来自子组件的信息 */
          sonMes:''      /* 发送给子组件的信息  */
       } 
   },
   methods:{
      /* 接受孙组件信息 */
      grandSonSay(value){
          this.grandSonMes = value
      },
      /* 接受子组件信息 */ 
      sonSay(value){
          this.sonMes = value
      },
   },
}
</script>

// 子组件
<template>
    <div class="son" >
        <input  v-model="mes"   /> <button @click="send"  >对父组件说</button>
        <grandSon />
    </div> 
</template>

<script>
import  grandSon from './grandSon'
export default {
    /* 子组件 */
   name:'son',
   components:{
       grandSon /* 孙组件 */
   },
   data(){
       return {
           mes:''
       }
   },
   /* 引入父组件 */
   inject:['father'],
   methods:{
       send(){
           this.father.sonSay(this.mes)
       }
   },
    
}
</script>
// 孙组件
<template>
   <div class="grandSon" >
        <input  v-model="mes"  /> <button @click="send"  >对爷爷组件说</button>
    </div> 
</template>

<script>
export default {
    /* 孙组件 */
   name:'grandSon',
   /* 引入爷爷组件 */
   inject:['father'],
   data(){
       return {
           mes:''
       }
   },
   methods:{
       send(){
           this.father.grandSonSay( this.mes )
       }
   }
}
</script>

暴露方法时绑定this

// 父组件
   provide(){
       return {
           /* 将通信方法暴露给子孙组件(注意绑定this) */
           grandSonSay:this.grandSonSay.bind(this),
           sonSay:this.sonSay.bind(this)
       }
   },   
   methods:{
      /* 接受孙组件信息 */
      grandSonSay(value){
          this.grandSonMes = value
      },
      /* 接受子组件信息 */ 
      sonSay(value){
          this.sonMes = value
      },
   },



// 子组件
/* 引入父组件方法 */
   inject:['sonSay'],
   methods:{
       send(){
           this.sonSay(this.mes)
       }
   },

6. 事件总线EventBus

优点: 不受组件层级的影响,可以实现任意两个组件的通信。需要数据就通过on绑定,传递数据就emit触发

缺点: 事件组件命名不利于维护

juejin.cn/post/689823…

7. vuex

缺点: vuex通信方式相比其他方式,比较复杂,而且如果不同的模块,需要建立独立的modules

优点

  1. 解决两个毫无干系的两个组件的通信问题
  2. 支持异步组件通信(vuex中actions允许我们做一些异步操作)

问题:为什么action可以实现异步?

因为里面底层通过Promise.resolve能够获取异步任务完成的状态。

juejin.cn/post/690600…

参考

my.oschina.net/u/3347851/b…