前言

在Vue组件库开发过程中,Vue组件之间的通信始终是一个重要的话题,尽管官网推出的 Vuex 状态治理计划能够很好的解决组件之间的通信问题,然而在组件库外部应用 Vuex 往往会比拟重,本文将零碎的列举出几种不应用 Vuex,比拟实用的组件间的通信形式,供大家参考。

组件之间通信的场景

在进入咱们明天的主题之前,咱们先来总结下Vue组件之间通信的几种场景,个别能够分为如下几种场景:

  1. 父子组件之间的通信
  2. 兄弟组件之间的通信
  3. 隔代组件之间的通信

父子组件之间的通信

父子组件之间的通信应该是 Vue 组件通信中最简略也最常见的一种了,概括为两个局部:父组件通过prop向子组件传递数据,子组件通过自定义事件向父组件传递数据。

父组件通过 prop 向子组件传递数据

Vue组件的数据流向都遵循单向数据流的准则,所有的 prop 都使得其父子 prop 之间造成了一个单向上行绑定:父级 prop 的更新会向下流动到子组件中,然而反过来则不行。这样会避免从子组件意外变更父级组件的状态,从而导致你的利用的数据流向难以了解。

额定的,每次父级组件产生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件外部扭转 prop。如果你这样做了,Vue 会在浏览器的控制台中收回正告。

父组件 ComponentA:

<template>  <div>    <component-b title="welcome"></component-b>  </div></template><script>import ComponentB from './ComponentB'export default {  name: 'ComponentA',  components: {    ComponentB  }}</script>

子组件 ComponentB:

<template>  <div>    <div>{{title}}</div>  </div></template><script>export default {  name: 'ComponentB',  props: {    title: {      type: String,    }  }} </script>

子组件通过自定义事件向父组件传递数据

在子组件中能够通过 $emit 向父组件产生一个事件,在父组件中通过 v-on/@ 进行监听。

子组件 ComponentA:

<template>  <div>    <component-b :title="title" @title-change="titleChange"></component-b>  </div></template><script>import ComponentB from './ComponentB'export default {  name: 'ComponentA',  components: {    ComponentB  },  data: {    title: 'Click me'  },  methods: {    titleChange(newTitle) {      this.title = newTitle    }   }}</script>

子组件 ComponentB:

<template>  <div>    <div @click="handleClick">{{title}}</div>  </div></template><script>export default {  name: 'ComponentB',  props: {    title: {      type: String,    }  },  methods: {    handleClick() {      this.$emit('title-change', 'New title !')    }    }} </script>

这个例子非常简单,在子组件 ComponentB 外面通过 $emit 派发一个事件 title-change,在父组件 ComponentA 通过 @title-change 绑定的 titleChange 事件进行监听,ComponentB 向 ComponentA 传递的数据在 titleChange 函数的传参中能够获取到。

兄弟组件之间的通信

状态晋升

写过 React 的同学应该对组件的 状态晋升 概念并不生疏,React 外面将组件依照职责的不同划分为两类:展现型组件(Presentational Component)容器型组件(Container Component)

展现型组件不关怀组件应用的数据是如何获取的,以及组件数据应该如何批改,它只须要晓得有了这些数据后,组件UI是什么样子的即可。内部组件通过 props 传递给展现型组件所需的数据和批改这些数据的回调函数,展现型组件只是它们的使用者。

容器型组件的职责是获取数据以及这些数据的解决逻辑,并把数据和逻辑通过 props 提供给子组件应用。

因而,参考 React 组件中的 状态晋升 的概念,咱们在两个兄弟组件之上提供一个父组件,相当于容器组件,负责解决数据,兄弟组件通过 props 接管参数以及回调函数,相当于展现组件,来解决兄弟组件之间的通信问题。

ComponentA(兄弟组件A):

<template>  <div>    <div>{{title}}</div>    <div @click="changeTitle">click me</div>  </div></template><script>export default {  name: 'ComponentA',  props: {    title: {      type: String    },    changeTitle: Function  }}</script>

ComponentB(兄弟组件B):

<template>  <div>    <div>{{title}}</div>    <div @click="changeTitle">click me</div>  </div></template><script>export default {  name: 'ComponentB',  props: {    title: {      type: String    },    changeTitle: Function  }}</script>

参考 前端进阶面试题具体解答

ComponentC(容器组件C):

<template>  <div>    <component-a :title="titleA" :change-title="titleAChange"></component-a>    <component-b :title="titleB" :change-title="titleBChange"></component-b>  </div></template><script>import ComponentA from './ComponentA'import ComponentB from './ComponentB'export default {  name: 'ComponentC',  components: {    ComponentA,    ComponentB  },  data: {    titleA: 'this is title A',    titleB: 'this is title B'  },  methods: {    titleAChange() {      this.titleA = 'change title A'    },    titleBChange() {      this.titleB = 'change title B'    }  }}</script>

能够看到,上述这种 "状态晋升" 的形式是比拟繁琐的,特地是兄弟组件的通信还要借助于父组件,组件简单之后解决起来是相当麻烦的。

隔代组件之间的通信

隔代组件之间的通信能够通过如下几种形式实现:

  • $attrs/$listeners
  • rovide/inject
  • 基于 $parent/$children 实现的 dispatchbroadcast

attrs/listeners

Vue 2.4.0 版本新增了 $attrs$listeners 两个办法。先看下官网对 $attrs 的介绍:

蕴含了父作用域中不作为 prop 被辨认 (且获取) 的 attribute 绑定(classstyle 除外)。当一个组件没有申明任何 prop 时,这里会蕴含所有父作用域的绑定 (classstyle 除外),并且能够通过 v-bind="$attrs" 传入外部组件——在创立高级别的组件时十分有用。

看个例子:

组件A(ComponentA):

<template>  <component-b name="Lin" age="24" sex="male"></component-b></template><script>import ComponentB from '@/components/ComponentB.vue'export default {  name: 'ComponentA',  components: {    ComponentB  }}</script>

组件B(ComponetB):

<template>  <div>    I am component B    <component-c v-bind="$attrs"></component-c>  </div></template><script>import ComponentC from '@/components/ComponentC.vue'export default {  name: 'ComponentB',  inheritAttrs: false,  components: {    ComponentC  }}</script>

组件C(ComponetC):

<template>  <div>    I am component C  </div></template><script>export default {  name: 'ComponentC',  props: {    name: {      type: String    }  },  mounted: function() {    console.log('$attrs', this.$attrs)  }}</script>

这里有三个组件,先人组件(ComponentA)、父组件(ComponentB)和子组件(ComponentC)。这三个组件形成了一个典型的子孙组件之间的关系。

ComponetA 给 ComponetB 传递了三个属性 name、age 和 sex,ComponentB 通过 v-bind="$attrs" 将这三个属性再透传给 ComponentC, 最初在 ComponentC 中打印 $attrs 的值为:

{age: '24', sex: 'male'}

为什么咱们一开始传递了三个属性,最初只打印了两个属性 age 和 sex 呢?因为在 ComponentC 的props 中申明了 name 属性,$attrs 会主动排除掉在 props 中申明的属性,并将其余属性以对象的模式输入。

说白了就是一句话,$attrs 能够获取父组件中绑定的非 Props 属性

个别在应用的时候会同时和 inheritAttrs 属性配合应用。

如果你不心愿组件的根元素继承 attribute,你能够在组件的选项中设置 inheritAttrs: false

在 ComponentB 增加了 inheritAttrs=false 属性后,ComponentB 的dom构造中能够看到是不会继承父组件传递过去的属性:

如果不加上 inheritAttrs=false 属性,就会主动继承父组件传递过去的属性:

再看下 $listeners 的定义:

蕴含了父作用域中的 (不含 .native 润饰器的) v-on 事件监听器。它能够通过 v-on="$listeners" 传入外部组件——在创立更高层次的组件时十分有用。

$listeners也能把父组件中对子组件的事件监听全副拿到,这样咱们就能用一个v-on把这些来自于父组件的事件监听传递到下一级组件。

持续革新 ComponentB 组件:

<template>  <div>    I am component B    <component-c v-bind="$attrs" v-on="$listeners"></component-c>  </div></template><script>import ComponentC from '@/components/ComponentC.vue'export default {  name: 'ComponentB',  inheritAttrs: false,  components: {    ComponentC  }}</script>

这里利用 $attrs$listeners 办法,能够将先人组件(ComponentA) 中的属性和事件透传给孙组件(ComponentC),这样就能够实现隔代组件之间的通信。

provide/inject

provide/inject 是 Vue 2.2.0 版本后新增的办法。

这对选项须要一起应用,以容许一个先人组件向其所有子孙后代注入一个依赖,不管组件档次有多深,并在其上下游关系成立的工夫里始终失效。如果你相熟 React,这与 React 的上下文个性很类似。

先看下简略的用法:

父级组件:

export default {  provide: {    name: 'Lin'  }}

子组件:

export default {  inject: ['name'],  mounted () {    console.log(this.name);  // Lin  }}

下面的例子能够看到,父组件通过 privide 返回的对象外面的值,在子组件中通过 inject 注入之后能够间接拜访到。

然而须要留神的是,provideinject 绑定并不是可响应的,依照官网的说法,这是刻意为之的

也就是说父组件 provide 外面的name属性值变动了,子组件中 this.name 获取到的值不变。

如果想让 provide 和 inject 变成可响应的,有以下两种形式:

  • provide 先人组件的实例,而后在子孙组件中注入依赖,这样就能够在子孙组件中间接批改先人组件的实例的属性,不过这种办法有个毛病就是这个实例上挂载很多没有必要的货色比方props,methods
  • 应用 Vue 2.6 提供的 Vue.observable 办法优化响应式 provide

看一下第一种场景:

先人组件组件(ComponentA):

export default {  name: 'ComponentA',  provide() {    return {      app: this    }  },  data() {    return {       appInfo: {         title: ''       }    }  },  methods: {    fetchAppInfo() {      this.appInfo = { title: 'Welcome to Vue world'}    }  }}

咱们把整个 ComponentA.vue 的实例 this 对外提供,命名为 app。接下来,任何组件只有通过 inject 注入 app 的话,都能够间接通过 this.app.xxx 来拜访 ComponentA.vue 的 datacomputedmethods 等内容。

子组件(ComponentB):

<template>  <div>    {{ title }}    <button @click="fetchInfo">获取App信息</button>  </div></template><script>export default {  name: 'ComponentB',  inject: ['app'],  computed: {    title() {      return this.app.appInfo.title    }  },  methods: {    fetchInfo() {      this.app.fetchAppInfo()    }   }}</script>

这样,任何子组件,只有通过 inject 注入 app 后,就能够间接拜访先人组件中的数据了,同时也能够调用先人组件提供的办法批改先人组件的数据并反馈到子组件上。

当点击子组件(ComponentB)的获取App信息按钮,会调用 this.app.fetchAppInfo 办法,也就是拜访先人组件(ComponentA)实例上的 fetchAppInfo 办法,fetchAppInfo 会批改fetchAppInfo的值。同时子组件(ComponentB)中会监听 this.app.appInfo 的变动,并将变动后的title值显示在组件上。

再看一下第二种场景,通过 Vue.observable 办法来实现 provideinject 绑定并可响应。

基于下面的示例,革新先人组件(ComponentA):

import Vue from 'vue'const state = Vue.observable({ title: '' });export default {  name: 'ComponentA',  provide() {    return {      state    }  }}

应用 Vue.observable 定义一个可响应的对象 state,并在 provide 中返回这个对象。

革新子组件(ComponentB):

<template>  <div>    {{ title }}    <button @click="fetchInfo">获取App信息</button>  </div></template><script>export default {  name: 'ComponentInject',  inject: ['state'],  computed: {    title() {      return this.state.title    }  },  methods: {    fetchInfo() {      this.state.title = 'Welcome to Vue world22'    }   }}</script>

与之前的例子不同的是,这里咱们间接批改了 this.state.title 的值,因为 state 被定义成了一个可响应的数据,所以 state.title 的值被批改后,视图上的 title 也会立刻响应并更新,从这里看,其实很像 Vuex 的解决形式。

以上两种形式比照能够发现,第二种借助于 Vue.observable 办法实现 provideinject 的可响应更加简略高效,举荐大家应用这种形式。

基于 $parent/$children 实现的 dispatchbroadcast

先理解下 dispatch 和 broadcast 两个概念。

  • dispatch: 派发,指的是从一个组件外部向上传递一个事件,并在组件外部通过 $on 进行监听
  • broadcast: 播送,指的是从一个组件外部向下传递一个事件,并在组件外部通过 $on 进行监听

在实现 dispatch 和 broadcast 办法之前,先来看一下具体的应用办法。有 ComponentA.vueComponentB.vue 两个组件,其中 ComponentB 是 ComponentA 的子组件,两头可能跨多级,在 ComponentA 中向 ComponentB 通信:

组件ComponentA:

<template>  <button @click="handleClick">派发事件</button></template><script>import Emitter from '../mixins/emitter.js';export default {  name: 'ComponentA',  mixins: [Emitter],  methods: {    handleClick () {      this.dispatch('ComponentB', 'on-message', 'Hello Vue.js')    }  }}</script>

组件ComponentB:

export default {  name: 'ComponentB',  created () {    this.$on('on-message', this.showMessage)  },  methods: {    showMessage (text) {      console.log(text)    }  }}

dispatch 的逻辑写在 emitter.js 中,应用的时候通过 mixins 混入到组件中,这样能够很好的将事件通信逻辑和组件进行解耦。

dispatch 的办法有三个传参,别离是:须要承受事件的组件的名字(全局惟一,用来准确查找组件)、事件名和事件传递的参数。

dispatch 的实现思路非常简单,通过 $parent 获取以后父组件对象,如果组件的name和承受事件的name统一(dispatch办法的第一个参数),在父组件上调用 $emit 发射一个事件,这样就会触发指标组件上 $on 定义的回调函数,如果以后组件的name和承受事件的name不统一,就递归地向上调用此逻辑。

dispath:

export default {  methods: {    dispatch(componentName, eventName, params) {      let parent = this.$parent || this.$root;      let name = parent.$options.name;      while (parent && (!name || name !== componentName)) {        parent = parent.$parent;        if (parent) {          name = parent.$options.name        }      }      if (parent) {        parent.$emit.apply(parent, [eventName].concat(params));      }    }  }}

broadcast逻辑和dispatch的逻辑差不多,只是一个是通过 $parent 向上查找,一个是通过 $children 向下查找,

export default {  methods: {    broadcast(componentName, eventName, params) {      this.$children.forEach(child => {        const name = child.$options.name        if (name === componentName) {          child.$emit.apply(child, [eventName].concat(params))        } else {          broadcast.apply(child, [componentName, eventName].concat([params]))        }      })    }  }}