Vue里组件之间的通信方式

组件之间的关系

一般来说,组件之间有以下几种关系:
Vue里组件之间的通信方式_第1张图片
A-B、B-C、B-D都是父子关系

C-D是兄弟关系
A-C、A-D都是隔代关系(有可能隔很多代)
针对不同的关系,我们在传递数据时要选择不同的方法。针对vue组件间通信的几种方式,如props、 $emit/ $on、vuex、 $parent / $children、 $attrs/ $listeners和provide/inject

props/$emit

props方法:父组件向子组件传递数据

父组件A向子组件B传递数据时,A组件通过v-bind这种绑定属性的方式传递数据,B组件通过props接收数据。

//App.vue父组件里
<template>
 <div>
      <users v-bind:user="users"></users>
      //前者自定义名称便于子组件调用,后者要传递数据名
 </div>
</template>

<script>
import Users from "./components/Users"
export default {
 name: 'App',
 data(){
   return{
     users:["Henry","Bucky","Emily"]
   }
 },
 components:{
   "users":Users
  }
}
//users子组件
<template>
 <div class="hello">
   <ul>
     <li v-for="user in users">{{user}}</li>//遍历传递过来的值,然后呈现到页面
   </ul>
 </div>
</template>

<script>
export default {
 name: 'HelloWorld',
 props:{
   user:{           //这个就是父组件中子标签自定义名字
     type:Array,
     required:true
   }
 }
}

</script>

说明:子组件通过props来接收父组件传递过来的数据,有三种方法接收,比如上例里,除了此种方法接收外,还可以这样接收:方法一:props:[‘user’] , 方法二: props:{ user: Array }

$emit方法:子组件向父组件传递数据
子组件B通过events给父组件B发送消息,实际上就是子组件把自己的数据发送到父组件.

// 子组件 Headerdemo.vue
<template>
  <div>
    <h1 @click="changeTitle">{{ title }}</h1> //绑定一个点击事件
  </div>
</template>

<script>
    export default {
      name: 'headerdemo',
      data() {
        return {
          title:"Vue.js Demo"
        }
      },
      methods:{
        changeTitle() {
          this.$emit("titleChanged","子向父组件传值"); //自定义事件  传递值“子向父组件传值”
        }
      }
    }

</script>
// 父组件
<template>
  <div>
    <Headerdemo  v-on:titleChanged="updateTitle"></Headerdemo>
    //与子组件titleChanged自定义事件保持一致
   // updateTitle($event)接受传递过来的文字
    <h2>{{ title2 }}</h2>
  </div>
</template>

<script>
import Headerdemo  from "./components/Headerdemo"
    export default {
      name: 'App',
      data(){
        return{
          title2:"传递的是一个值"
        }
      },
      methods:{
        updateTitle(e){   //声明这个函数
          this.title2 = e;
        }
      },
      components:{
         Headerdemo
      }
    }
</script>

结果如下:
在这里插入图片描述
当我点击"Vue.js Demo"后,会变成如下结果:
在这里插入图片描述
有点需要注意的是组件的命名,比如这里我用的Headerdemo命名子组件,而H5新增的标签Header是不能作为组件命名的,同理,H5新增的footer , nav等也是不能作为组件命名的,因为会起冲突。

$emit / $on = bus

兄弟组件之间的传值可以先子传父然后父传子,借助父组件搭桥实现传值。除此之外还能借助中央事件总线,具体实现方式如下:
var Event=new Vue ( );
Event.$ emit(事件名,数据);
Event.$on(事件名,data => {});
vue实例 作为事件总线(事件中心)用来触发事件和监听事件,可以通过此种方式进行组件间通信包括:父子组件、兄弟组件、跨级组件.不过我们一般用于兄弟组件之间的传值。看下例:
如下图,在assets文件夹下创建bus.js文件
import Vue from ‘vue’
export defult new Vue( )
Vue里组件之间的通信方式_第2张图片
两个组件作为子组件被同一个父组件所引用,在同一个页面显示,
如下图所示,注册并引入两个组件my-b1和my-b2:
Vue里组件之间的通信方式_第3张图片再来看my-b1和my-b2这两个兄弟组件是如何写的:
Vue里组件之间的通信方式_第4张图片
Vue里组件之间的通信方式_第5张图片
两个兄弟组件之间的语法:

My-B1这个组件需要定义一个发送数据的方法,此处sendMsg是定义在此的方法,页面点击时候触发此函数,函数主体,通过Bus中央事件总线用$emit发送一个send事件,事件的功能就是传送一个"我是My-B1向My-B2传递的值"数据,区分sendMsg和send,前者是当前页面的点击事件,后者是发送出去,需要其他组件监听的事件。

My-B2在组件My-B2事先定义一个变量,在生命周期为created的时候,通过Bus中央事件总线用$on监听组件My-B1中发送的send事件,用一个带参数的回调函数,接收穿过来的值,参数即为传的值 ,把值赋值给当前组件的变量。

我们来看下效果:
Vue里组件之间的通信方式_第6张图片
当我点击组件My-B1里的按钮后,效果如下:
Vue里组件之间的通信方式_第7张图片

vuex

Vue里组件之间的通信方式_第8张图片
vuex原理:
Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。

各模块在流程中的功能介绍:
Vue Components:Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。

dispatch:操作行为触发方法,是唯一能执行action的方法。

actions:操作行为处理模块,由组件中的 $store.dispatch(‘action 名称’,data1)来触发。然后由commit()来触发mutation的调用 , 间接更新 state。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。

commit:状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。

mutations:状态改变操作方法,由actions中的 commit(‘mutation 名称’)来触发。是Vuex修改state的唯一推荐方法。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。

state:页面状态管理容器对象。集中存储Vue components中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。

getters:state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。

**state**:vuex的唯一数据源,如果获取多个state,可以使用...mapState。
export const store = new Vuex.Store({   
// 注意Store的S大写
<!-- 状态储存 -->
   state: {
        productList: [
           {
           name: 'goods 1',
           price: 100
           }
       ]
   }
})

**getter:** 可以将getter理解为计算属性,getter的返回值根据他的依赖缓存起来,依赖发生变化才会被重新计算。

import Vue from 'vue'
import Vuex from 'vuex';
Vue.use(Vuex)

export const store = new Vuex.Store({ 
    state: {
        productList: [
            {
            name: 'goods 1',
            price: 100
            },
        ]
    },

    // 辅助对象 mapGetter
    getters: {
        getSaledPrice: (state) => {
            let saleProduct = state.productList.map((item) => {
                return {
                    name: '**' + item.name + '**',
                    price: item.price / 2
                }
            })
            return saleProduct;
        }
    }
})

// 获取getter计算后的值
export default {
    data () {
        return {
            productList : this.$store.getters.getSaledPrice 
        }
    }
}

**mutation**:更改vuex的state中唯一的方是提交mutation都有一个字符串和一个回调函数。回调函数就是使劲进行状态修改的地方。并且会接收state作为第一个参数payload为第二个参数,payload为自定义函数,mutation必须是同步函数。

// 辅助对象 mapMutations
mutations: {
    <!-- payload 为自定义函数名-->
    reducePrice: (state, payload) => {
        return state.productList.forEach((product) => {
            product.price -= payload;
        })
    }
}

<!-- 页面使用 -->
methods: {
    reducePrice(){
        this.$store.commit('reducePrice', 4)
    }
}


**action**:action类似mutation都是修改状态,不同之处是action提交的mutation不是直接修改状态,action可以包含异步操作,而mutation不行。action中的回调函数第一个参数是context,是一个与store实例具有相同属性的方法的对象,action通过store.dispatch触发,mutation通过store.commit提交。

actions: {   
// 提交的是mutation,可以包含异步操作
    reducePriceAsync: (context, payload) => {
        setTimeout(()=> {
            context.commit('reducePrice', payload);  // reducePrice为上一步mutation中的属性
        },2000)
    }
}

<!-- 页面使用 -->
// 辅助对象 mapActions
methods: {
  reducePriceAsync(){
        this.$store.dispatch('reducePriceAsync', 2)
   },
}


**module**:由于是使用单一状态树,应用的所有状态集中到比较大的对象,当应用变得非常复杂是,store对象就有可能变得相当臃肿。为了解决以上问题,vuex允许我们将store分割成模块,每个模块拥有自己的state,mutation,action,getter,甚至是嵌套子模块从上至下进行同样方式分割。

const moduleA = {
    state: {...},
    mutations: {...},
    actions: {...},
    getters: {...}
}

const moduleB = {
    state: {...},
    mutations: {...},
    actions: {...},
    getters: {...}
}
const store = new Vuex.Store({
    a: moduleA,
    b: moduleB
})
store.state.a
store.state.b

vuex中数据存储localstorage
vuex 是 vue 的状态管理器,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到了初始状态,具体做法应该在vuex里数据改变的时候把数据拷贝一份保存到localStorage里面,刷新之后,如果localStorage里有保存的数据,取出来再替换store里的state。

例如:

let defaultCity = "上海"
try {    
// 用户关闭了本地存储功能,此时在外层加个try...catch
  if (!defaultCity){
  // f复制一份
        defaultCity = JSON.parse(window.localStorage.getItem('defaultCity'))
        }
    }catch(e){
        console.log(e)
    }
export default new Vuex.Store({
  state: {
    city: defaultCity
  },
  mutations: {
    changeCity(state, city) {
      state.city = city
      try {
      window.localStorage.setItem('defaultCity', JSON.stringify(state.city));
      // 数据改变的时候把数据拷贝一份保存到localStorage里面
      } catch (e) {}
    }
  }
})

注意:vuex里,保存的状态,都是数组,而localStorage只支持字符串,所以需要用JSON转换:
JSON.stringify(state.subscribeList) // array -> string
JSON.parse(window.localStorage.getItem(“subscribeList”))// string -> array

$attrs/ $listeners

多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此Vue2.4 版本提供了另一种方法---- $attrs/ $listeners

$ attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 interitAttrs 选项一起使用。

$ listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。
接下来我们看个跨级通信的例子:

// index.vue
<template>
  <div>
    <h2>王者峡谷</h2>
    <child-com1 :foo="foo" :boo="boo" :coo="coo" :doo="doo" title="加油加油"></child-com1>
  <hr style="color:red" />
  </div>
</template>

<script>
    const childCom1 = () => import("./childCom1.vue");
    export default {
      components: { childCom1 },
      data() {
        return {
          foo: "Javascript",
          boo: "Html",
          coo: "CSS",
          doo: "Vue"
        };
      }
    };
</script>

//childCom1.vue
<template class="border">
  <div>
    <p>foo: {{ foo }}</p>
    <p>childCom1的$attrs: {{ $attrs }}</p>
   <hr style="color:red" />
    <child-com2 v-bind="$attrs"></child-com2>
  </div>
</template>

<script>
    const childCom2 = () => import("./childCom2.vue");
    export default {
      components: {
        childCom2
      },
      inheritAttrs: false, // 可以关闭自动挂载到组件根元素上的没有在props声明的属性
      props: {
        foo: String // foo作为props属性绑定
      },
      created() {
        console.log(this.$attrs); 
        // { "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "加油加油" }
      }
    };
</script>


// childCom2.vue
<template>
  <div class="border">
    <p>boo: {{ boo }}</p>
    <p>childCom2: {{ $attrs }}</p>
<hr style="color:red" />
    <child-com3 v-bind="$attrs"></child-com3>
  </div>
</template>

<script>
const childCom3 = () => import("./childCom3.vue");
export default {
  components: {
    childCom3
  },
  inheritAttrs: false,
  props: {
    boo: String
  },
  created() {
    console.log(this.$attrs); 
    // {"coo": "CSS", "doo": "Vue", "title": "加油加油" }
  }
};
</script>


// childCom3.vue
<template>
  <div class="border">
    <p>childCom3: {{ $attrs }}</p>
  </div>
</template>

<script>
    export default {
      props: {
        coo: String,
        title: String
      }
    };
</script>

所示$attrs表示没有继承数据的对象,格式为{属性名:属性值}。Vue2.4提供了$attrs , $listeners 来传递数据与事件,跨级组件之间的通讯变得更简单。
简单来说:$attrs与$listeners 是两个对象,$attrs 里存放的是父组件中绑定的非 Props 属性,$listeners里存放的是父组件中绑定的非原生事件。
6provide/inject

provide/inject

Vue2.2.0新增API,这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。一言而蔽之:祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。
provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

//A.vue
export default {
    provide: {
        name: '我是祖先组件'
    }
}

// N.vue
<template>
    <div>{{ name }} </div>
</template>

export default {
    inject: ['name'],
    mounted () {
        console.log(this.name) //输出我是祖先组件
    }
}

A.vue,我们设置了一个 provide:name,将name这个变量提供给它的所有子孙组件,这样不管中间隔了B.vue、C.vue等等,不管传了多少代,子孙组件总是能获取到祖先组件A.vue传过来的值name。需要注意的是,provide 和 inject 绑定并不是可响应的。这是刻意为之的,也就是说,上面 A.vue 的 name 如果改变了,B.vue的 this.name 是不会改变的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

provide与inject 怎么实现数据响应式?
有两种方法可以实现provide与inject的响应式。
方法一:
provide祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西比如props,methods.

方法二:
使用2.6最新API Vue.observable 优化响应式 provide(推荐)
例如:

组件D、E和F获取A组件传递过来的color值,并能实现数据响应式变化,即A组件的color变化后,组件D、E、F会跟着变(核心代码如下:)
Vue里组件之间的通信方式_第9张图片

// A 组件 

<div>
      <h1>A 组件</h1>
      <button @click="() => changeColor()">改变color</button>
      <ChildrenB />
      <ChildrenC />
</div>

......
  data() {
    return {
      color: "blue"
    };
  },

  // provide() {
  //   return {
  //     theme: {
  //       color: this.color //这种方式绑定的数据并不是可响应的
  //     } // 即A组件的color变化后,组件D、E、F不会跟着变
  //   };
  // },

  provide() {
    return {
      theme: this//方法一:提供祖先组件的实例
    };
  },

  methods: {
    changeColor(color) {
      if (color) {
        this.color = color;
      } else {
        this.color = this.color === "blue" ? "red" : "blue";
      }
    }
  }

  // 方法二:使用2.6最新API Vue.observable 优化响应式 provide
  // provide() {
  //   this.theme = Vue.observable({
  //     color: "blue"
  //   });
  //   return {
  //     theme: this.theme
  //   };
  // },

  // methods: {
  //   changeColor(color) {
  //     if (color) {
  //       this.theme.color = color;
  //     } else {
  //       this.theme.color = this.theme.color === "blue" ? "red" : "blue";
  //     }
  //   }
  // }

// F 组件 

<template functional>
<div class="border2">
  <h3 :style="{ color: injections.theme.color }">F 组件</h3>
</div>
</template>

<script>
export default {
inject: {
  theme: {
    //函数式组件取值不一样
    default: () => ({})
  }
}
};
</script>

$parent / $children 与 ref

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
$ parent / $ children:访问父 / 子实例,这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。
使用 this.$ refs查找命名子组件。我们先来看个用 ref来访问组件和DOM元素的例子:
Vue里组件之间的通信方式_第10张图片
使用 this.$ parent查找当前组件的父组件。
使用 this.$children查找当前组件的直接子组件,可以遍历全部子组件, 需要注意 $ children 并不保证顺序,也不是响应式的。
使用 this.$ root查找根组件,并可以配合$children遍历全部组件。
例子:

//父组件Game.vue
<template>
<div>
    <h2>{{ msg }}</h2>
    <LOL ref="lol"></LOL>
    <DNF ref="dnf"></DNF>
</div>
</template>

<script>
import LOL from '@/components/game/LOL'
import DNF from '@/components/game/DNF'

export default {
    name: 'game',
    components: {
        LOL,
        DNF
    },
    data () {
        return {
            msg: 'Game',
            lolMsg:'Game->LOL',
            dnfMsg:'Game->DNF',
        }
    },
    methods: {
    },
    mounted(){ //注意 mounted
        //读取子组件数据,注意$children子组件的排序是不安全的
        console.log(this.$children[0].gameMsg);//LOL->Game
        //读取命名子组件数据
        console.log(this.$refs.dnf.gameMsg);//DNF->Game
        //从根组件查找组件数据
        console.log(this.$root.$children[0].msg); //APP
        console.log(this.$root.$children[0].$children[0].msg); //Game
   console.log(this.$root.$children[0].$children[0].$children[0].msg); //Game->LOL
   console.log(this.$root.$children[0].$children[0].$children[1].msg); //Game->DNF
    }
}
</script>

<style>
.game{
    border: 1px solid #00FF00;
    width: 200px;
}  
</style>


//子组件LOL.vue
<template>
  <div class="lol">
    <h2>{{ msg }}</h2>
  </div>
</template>

<script>
export default {
    name: 'LOL',
    data () {
        return {
            msg: 'LOL',
            gameMsg:'LOL->Game',
        }
    },
    methods:{
    },
    created(){
        //读取父组件数据
        this.msg = this.$parent.lolMsg;
    }
}
</script>


//子组件DNF.vue
<template>
  <div class="dnf">
    <h2>{{ msg }}</h2>
  </div>
</template>

<script>
import Bus from '../../utils/bus.js'
export default {
    name: 'DNF',
    data () {
        return {
            msg: 'DNF',
            gameMsg:'DNF->Game',
        }
    },
    methods:{
    },
    created(){
        //从根组件向下查找父组件数据
        this.msg = this.$root.$children[0].$children[0].dnfMsg;
    }
}
</script>

总结

Vue里组件通信常见使用场景可以分为三类:

父子通信: 父向子传递数据是通过 props,子向父是通过 events( $emit);通过父链 / 子链也可以通信( $parent / $children);ref 也可以访问组件实例;provide / inject API; $attrs / $ listeners
兄弟通信: Bus;Vuex
跨级通信:Bus;Vuex;provide / inject API、$ attrs/$ listeners

Vue里组件之间的通信方式_第11张图片

你可能感兴趣的:(Vue)