vue从入门到精通之【组件通信和插槽】(六)

1,516 阅读5分钟

父子组件的通信

首先,我们指导正常情况下子组件是不能访问父组件或者Vue实例的数据的

但是,在开发中,往往一些数据确实需要从上层传递到下层

  • 比如在一个页面中,我们从服务器请求到了很多的数据。
  • 其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
  • 这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)

那如何进行父子组件间的通信呢?Vue官方提到

  • 通过props向子组件传递数据
  • 通过事件向父组件发送消息

在下面的代码中,我直接将Vue实例当做父组件,并且其中包含子组件来简化代码。

真实的开发中,Vue实例和子组件的通信和父组件和子组件的通信过程是一样的。

props基本用法

在组件中,使用选项props来声明需要从父级接收到的数据。

props的值有两种方式:

  • 方式一:字符串数组,数组中的字符串就是传递时的名称。
  • 方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。

我们先来看一个最简单的props传递:

<div id="app">
  <child-cpn :message="message"></child-cpn>
</div>

<template id="child-cpn">
  <div>显示的信息: {{message}}</div>
</template>

<script>
  let app  = new Vue({
    el: '#app',
    data: {
      message: 'Hello lazy'
    },
    components: {
      'child-cpn': {
        template: '#child-cpn',
        props: ['message']
      }
    }
  })
</script>

步骤

  • Vue实例中的data
  • 子组件的props
  • 通过:message='message'将data中的数据传给props
  • 将props中的数据显示在子组件中

props数据验证

在前面,我们的props选项是使用一个数组。

我们说过,除了数组之外,我们也可以使用对象,当需要对props进行类型等验证时,就需要对象写法了。

验证都支持哪些数据类型呢?

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
Vue.components('my-components',{
  props: {
    // 基础的类型检查('null' 匹配任何类型)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function (){
        return {message: 'Hello'}
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})

当我们有自定义构造函数时,验证也支持自定义的类型

function Person (firstName, LastName){
  this.firstName = firstName
  this.lastName = lastName
}

Vue.component('blog-post', {
  props: {
    author: Person
  }
})

子级向父级传递

props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。

我们应该如何处理呢?这个时候,我们需要使用自定义事件来完成。

什么时候需要自定义事件呢?

  • 当子组件需要向父组件传递数据时,就要用到自定义事件了。
  • 我们之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。

自定义事件的流程:

  • 在子组件中,通过$emit()来触发事件。
  • 在父组件中,通过v-on来监听子组件事件。
<!--父组件模板-->
<div id="app">
  <!-- 监听子组件事件item-click -->
  <cpn @item-click="cpnClick"></cpn>
</div>

<!--子组件模板-->
<template id="cpn">
  <div>
    <button v-for="item in categories"
            @click="btnClick(item)">
      {{item.name}}
    </button>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>

  // 1.子组件
  const cpn = {
    template: '#cpn',
    data() {
      return {
        categories: [
          {id: 'aaa', name: '热门推荐'},
          {id: 'bbb', name: '手机数码'},
          {id: 'ccc', name: '家用家电'},
          {id: 'ddd', name: '电脑办公'},
        ]
      }
    },
    methods: {
      btnClick(item) {
        // 发射事件: 自定义事件
        this.$emit('item-click', item)
      }
    }
  }

  // 2.父组件
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      cpn
    },
    methods: {
      <!-- 定义好一个函数 -->
      cpnClick(item) {
        console.log('cpnClick', item.id, item.name);
      }
    }
  })
</script>

子组件cpn发射自定义事件item-click,携带参数item

父组件app在methods中定义一个函数

父组件app用v-on监听子组件事件item-click,并将事件指向一个在methods中定义好的函数

父子组件的访问方式: $children

有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问跟组件。

  • 父组件访问子组件:使用childrenchildren或refs
  • 子组件访问父组件:使用$parent

$children的缺陷:

  • 通过$children访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。
  • 但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。
  • 有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用$refs

父子组件的访问方式: $refs

$refs的使用:

  • $refs和ref指令通常是一起使用的。
  • 首先,我们通过ref给某一个子组件绑定一个特定的ID。
  • 其次,通过this.$refs.ID就可以访问到该组件了。
<child-cpn1 ref="child1"></child-cpn1>
<child-cpn2 ref="child2"></child-cpn2>
<button @click="showRefsCpn"></button>

showRefscpn() {
      console.log(this.$refs.child1.message);
       console.log(this.$refs.child2.message);
    }

父子组件的访问方式: $parent

如果我们想在子组件中直接访问父组件,可以通过$parent

注意事项:

  • 尽管在Vue开发中,我们允许通过$parent来访问父组件,但是在真实开发中尽量不要这样做。
  • 子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。
  • 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。
  • 另外,更不好做的是通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护

非父子组件通信

刚才我们讨论的都是父子组件间的通信,那如果是非父子关系呢?

  • 非父子组件关系包括多个层级的组件,也包括兄弟组件的关系。

在Vue1.x的时候,可以通过dispatchdispatch和broadcast完成

  • $dispatch用于向上级派发事件
  • $broadcast用于向下级广播事件
  • 但是在Vue2.x都被取消了

在Vue2.x中,有一种方案是通过中央事件总线,也就是一个中介来完成。

  • 但是这种方案和直接使用Vuex的状态管理方案还是逊色很多。

插槽 slot

slot的基本使用

编译作用域

官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。

在子组件中,使用特殊的元素就可以为子组件开启一个插槽。

该插槽插入什么内容取决于父组件如何使用。

<div id='app'>
    <cpn><button>按钮</button></cpn>
    <cpn><span>哈哈哈</span></cpn>
    <cpn>
        <i>hehe</i>
        <div>lala</div>
        <p>xixi</p>
    </cpn>
    <cpn></cpn>
</div>

<template id='cpn'>
    <div>
        <h2>我是组件</h2>
        <p>我是组件,哈哈哈</p>
        <slot></slot>
        <!-- <button>按钮</button> -->
    </div>
</template>
<script>

const app = new Vue({
    el:'#app',
    data:{
  },
  components:{
      cpn:{
          template:'#cpn'
      }
  }
})

</script>  
  • 插槽的基本使用solt /solt
  • 插槽的默认值 slot button /solt
  • 如果有多个默认值,同时放入到组件进行替换时,作为替换元素

具名插槽slot

当子组件的功能复杂时,子组件的插槽可能并非是一个。

-比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别-代表左边、中间、右边。

-那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?

-这个时候,我们就需要给插槽起一个名字

-如何使用具名插槽呢?

-非常简单,只要给slot元素一个name属性即可

<div id='app'>
    <cpn><button  slot="left">anniu</button></cpn>
</div>

<template id='cpn'>
    <div>
        <slot name="left">左边</slot>
        <slot name="center">中间</slot>
        <slot name="right">右边</slot>
        <!-- <button>按钮</button> -->
    </div>
</template>
<script>
const app = new Vue({
    el:'#app',
    data:{
  },
  components:{
      cpn:{
          template:'#cpn'
      }
  }
})
</script>  

vue组件之间通信总结(超详细)

组件通信在我们平时开发过程中,特别是在vue和在react中,有着举足轻重的地位。本篇将总结在vue中,组件之间通信的几种方式:

  • props、$emit

  • parentparent、children

  • attrsattrs、listeners

  • provide、inject

  • eventBus

  • vuex

  • 本地存储

    props、$emit单向数据流

father.vue:

<template>
  <div>
    <div>我是父亲:<input type="button" value="父亲" /> 数字为: {{num}}</div>
    <son :num="num" @change="change"></son>
  </div>
</template>

<script>
import son from "./son.vue";
export default {
  name: "Father",
  components: {
    son,
  },
  data() {
    return {
      num: 1,
    };
  },
  methods:{
    change(val){
      this.num = val
    }
  }
};
</script>

son.vue:

<template>
  <div>我是儿子:<input type="button" value="儿子" @click="change"/>数字为:{{num}}</div>
</template>

<script>
export default {
  name: "App",
  components: {},
  props: {
    num: {
      default: 0,
    },
  },
  created() {},
  methods: {
    change(){
      // this.num = 2  props通信是单向数据流,在这直接修改父组件传过来的num将会报错
    // 可以用$emit触发change事件,father组件绑定change事件
    this.$emit('change', 2)

    }
  },
};
</script>

对于上面的场景:子组件的change事件只是为了修改父组件中某一个值,还可以有以下几种写法:

  1. 父组件绑定给子组件的事件使用箭头函数
father:
<son :num="num" @change="val => num = val"></son>

son:
this.$emit('change', 2)  
  1. update:num和.sync
father:

<son :num.sync="num"></son>

son:

this.$emit('update:num', 2)//update是规定的写法,不可更换
  1. v-model

先修改props和绑定的事件:

father:
<son :value="num" @input="val => num = val"></son>

son:
this.$emit('input', 2) 
可用v-model简写:

<son v-model="num"></son>

parentparent、children

parentparent、children可直接在父子组件中调用各自的方法以及修改数据

子组件中直接:this.$parent.num = 2

父组件中children是个数组,因此具体是哪个子组件不太直观,可以用children是个数组,因此具体是哪个子组件不太直观,可以用refs来操作子组件

vue官方并不推荐使用这种通信方式:节制地使用 parentparent 和 children - 它们的主要目的是作为访问组件的应急方法,更推荐用 props 和 events 实现父子组件通信。

attrsattrs、listeners

$attrs

包含了父作用域中不作为prop识别的特性绑定(除了class 和 style ),如果没有声明prop ,就会包含父作用域所有的绑定,传入内部组件

简而言之就是 接收除了props声明外的所有绑定属性(class , style 除外)

//一级 ------------------------------------------------------------------------------------------------------------
<div class="details">
      <h1>父组件---{{msg}}</h1>
      <myComponent :name="msg" age="12" gender="man" ></myComponent>
</div>

//二级 ------------------------------------------------------------------------------------------------------------
<--  <div :name="msg" age="12" gender="man" class="child">  -->
<div class="child">
    <h2>子组件----{{$attrs.name}}</h2>
    <Son v-bind="$attrs"/>
</div>

 created () {
    window.console.log(this.$attrs); //{name: "小白", age: "12", gender: "man"}
 } ,
//如果组件内部显示的声明了props , 那么$attrs 中就只包含除了声明的props之外的属性{age: "12", gender: "man"}
//props: ["name"],

//三级 ------------------------------------------------------------------------------------------------------------
 <div class="son">
      <h3>孙组件---{{$attrs.age}}</h3>
 </div>

created () {
    window.console.log(this.$attrs); //{name: "小白", age: "12", gender: "man"}
 } ,  

$listeners

官网解释 : 包含了父作用域中v-on事件监听器(不包含 .native修饰器的),可以通过v-on=$listeners 传入内部组件,在创建更高层次的组件时很有用

个人理解 :父作用域中的事件(除了 .native之外)可以传入到内部组件(可以是一级 , 二级 ,,,多级),使内部组件的数据传递到父级组件更加方便

//一级 ------------------------------------------------------------------------------------------------------------
<div class="details">
      <h1>父组件---{{msg}}</h1>
      <myComponent @name="getname" @age="getage" @gender="getgender" ></myComponent>
 </div>

methods: {
    getname(e){
      window.console.log("name",e) // name 小编
    },
    getage(){
      window.console.log("age")
    },
    getgender(){
      window.console.log("gender")
  }
//二级 ------------------------------------------------------------------------------------------------------------
<div class="child">
    <Son v-on="$listeners"/>
</div>

 created () {
    window.console.log(this.$listeners) // {name: ƒ, age: ƒ, gender: ƒ} 
  } ,      

//三级 ------------------------------------------------------------------------------------------------------------
<div class="son">
    <h3 v-on="$listeners"></h3>
    <button @click="toname">点击</button>
</div>      
    
 data() {
    return {
      name: "小编"
    };
  },
 created() {
    window.console.log(this.$listeners);
  },
  methods: {
    toname() {
      this.$listeners.name(this.name);
    }
  } 

案例

将element ui 的分页器封装成一个组件,使用的attrsattrs 和 listeners是原本是第三方组件的属性和事件可以在自己封装的组件中使用,(可以参考官网的element ui 分页器对比属性和事件的写法)

//组件
 <Pagenation
      v-if="list && list.length > 0"
      class="pagenation"
      :total="total"
      :page-size="pagesize"
      @current-change="onPageChange"
      :current-page.sync="currentPage"
    />

//element 组件
 <el-pagination 
  layout="prev, pager, next" 
  v-bind="$attrs" 
  v-on="$listeners">
  </el-pagination>  

provide、inject

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效

provide选项应该是一个对象或返回一个对象的函数。

inject选项应该是一个字符串数组或一个对象。

App:

  ...

export default {
  provide(){
    return {vm: this}
  },

...

son:

...

export default {
  inject: ['vm'],

 data(){},

 mounted(){
  console.log(this.vm)
 }

...

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

inject注入中的值会沿着组件向上查找,遵从"就近原则"。

平时开发不建议使用,过多的注入依赖,不方便管理的同时,嵌套关系复杂的话,也容易造成冲突

provide 和 inject中的数据流是双向的。

eventBus(事件总线)

eventBus通过发布订阅全局事件,供其他组件使用。

在main.js中:

Vue.prototype.$bus = new Vue();  

parent.vue:

<template>
  <div>
    <son1></son1>
    <son2></son2>
  </div>
</template>

<script>
import son1 from './son1.vue'
import son2 from './son2.vue'
export default {
  name: 'parent',
  components: {
    son1,
    son2
  },
  created(){
     this.$bus.$on('busEvent',(v)=>{
      console.log(v);
    })
  },
  beforeDestroy(){
    this.$bus.off('busEvent')
  }
}
</script>

son1和son2中的mounted:

 son1:
mounted(){
  this.$bus.$emit('busEvent','son1哈哈')
}

son2:
mounted(){
  this.$bus.$emit('busEvent', 'son2嘻嘻')
} 

使用eventBus有三点需要注意:

1.$bus.on应该在created钩子内使用,如果在mounted使用,它可能接收不到其他组件来自created钩子内发出的事件;

2.bus.emit应该在mounted中使用,等待created中的bus.emit应该在mounted中使用,等待created中的bus.on事件绑定完成;

3.发布订阅的事件在beforeDestory钩子里需要使用$bus.off解除,组件销毁后没必要一直监听。

vuex

借助vuex的状态管理来实现组件通信,vuex适用于较为复杂的项目,频繁的数据共享且数据量比较大。

store/index.js:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    isLogin: false
  },
  mutations: {
    loginState (state, isLogin) {
      state.isLogin = isLogin
    }
  }
})

export default store

App.vue:

created(){
  this.$store.commit('loginState',true)// 设置登录状态为true
},

son.vue:

<template>
  <div>我是儿子:<input type="button" value="儿子" />登录状态:{{isLogin}}</div>
</template>

<script>
import {mapState} from 'vuex';
export default {
  name: "son",
  computed:{
    ...mapState(['isLogin'])
  }
};
</script>

localstorage

localstorage是浏览器的本地存储,将会长期存储在浏览器中,非常庞大的数据不建议用此方式。

App.vue

created(){
  localStorage.setItem('isLogin', true)
},

son.vue:

computed:{
  isLogin(){
    return localStorage.getItem('isLogin')
  }
}