Vue: 组件间的通信方式

122 阅读4分钟

一、组件间通信的几种方式

  1. props/$emit
  2. $parent/$children
  3. provide/inject
  4. ref
  5. eventBus 中央事件总线
  6. v-slot
  7. v-model
  8. .sync修饰符
  9. Vuex
  10. $attrs/$listeners
  11. localStorage/sessionStorage

二、组件间通信的分类

  1. 父子间通信
传值方式方法
父传子props$parentprovide/injectv-slotv-model.sync 修饰符$attrs/$listeners
子传父$emit$children
父子互传refeventBus 中央事件总线VuexlocalStorage/sessionStorage
  1. 兄弟间通信
传值方式方法
兄弟互传eventBus 中央事件总线VuexlocalStorage/sessionStorage
  1. 跨组件通信
传值方式方法
兄弟互传provide/injecteventBus 中央事件总线Vuex$attrs/$listenerslocalStorage/sessionStorage

三、通信方式详解

1、props/$emit

  • 父组件通过props向子组件传递数据
  • 子组件通过$emit向父组件发送数据

父组件

<template>
  <son :data="sonData" @changeData="changeText"/>
</template>

<script>
import son from './son'
export default {
  name: 'parent',
  components: {son},
  data () {
    return {
      sonData: 'son data'
    }
  },
  methods: {
    changeText (txt) {
      this.sonData = txt
    }
  }
}
</script>

子组件 son.vue

<template>
  <div class="box">
    <div class="txt">{{data}}</div>
    <button @click="toChange">click</button>
  </div>
</template>

<script>
export default {
  name: 'son',
  props: ['data'],
  methods: {
    toChange () {
      this.$emit('changeData', 'son data change')
    }
  }
}
</script>

注意:

  • props 只允许父级向子级传递,不允许跨级传递;只读不可修改,所有的修改都会失效和报错
  • $emit 接收两个参数(arg1, arg2), arg1为传递数据的事件,arg2为需要传递的数据
  • 父组件 需要在引用的子组件上添加 ‘@传递数据的事件’ 属性来获取$emit发送的数据

2、$parent/$children

  • 子组件通过$parent直接访问父组件的实例
  • 父组件通过$children访问所有的子组件实例,并且可以递归向上或向下无限访问,直到根实例或者最内层组件

父组件

<template>
  <div class="box">
    <son />
    <button @click="toClick">click</button>
  </div>
</template>

<script>
import son from './son'
export default {
  name: 'ParentChildren',
  components: {son},
  data () {
    return {
      parent: '$parent/$children parent'
    }
  },
  methods: {
    toClick () {
      this.$children[0].changeSon()
    },
    changeParent () {
      this.parent += ' change'
    }
  }
}
</script>

子组件 son.vue

<template>
  <div class="box">
    <div class="text">{{son}}</div>
    <div class="text">{{parent}}</div>
    <button @click="toChange">change</button>
  </div>
</template>

<script>
export default {
  name: 'ParentChildrenSon',
  data() {
    return {
      son: '$parent/$children son'
    }
  },
  computed: {
    parent() {
      return this.$parent.parent
    }
  },
  methods: {
    toChange () {
      this.$parent.changeParent()
    },
    changeSon () {
      this.son += ' change'
    }
  }
}
</script>

注意:

  • 虽然Vue允许递归向上或者向下无限访问,但实际操作中不建议这么做,因为这种操作会使父子组件紧耦合,而且会使父组件的状态因为可能被任意组件修改而难以理解
  • $children并不保证顺序,也不是响应式

3、provide/inject

  • 允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效
  • 解决了跨域组件间的通讯问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系

祖先组件

<template>
  <parent />
</template>

<script>
import parent from './parent'

export default {
  name: 'ProvideInjectRoot',
  components: {parent},
  data () {
    return {
      content: 'provide inject root'
    }
  },
  provide () {
    return {
      text: this.content
    }
  }
}
</script>

父组件 parent.vue

<template>
  <div class="box">
    <div class="root">root: {{text}}</div>
    <div class="parent">parent: {{textCur}}</div>
    <son class="son" />
  </div>
</template>

<script>
import son from './son'
export default {
  name: 'ProvideInjectParent',
  components: {son},
  inject: ['text'],
  provide () {
    return {
      parent: this.textCur
    }
  },
  computed: {
    textCur() {
      return `${this.text}'s child`
    }
  }
}
</script>

子组件 son.vue

<template>
  <div class="box">
    <div class="root">root: {{text}}</div>
    <div class="parent">parent: {{parent}}</div>
  </div>
</template>

<script>
export default {
  name: 'ProvideInjectSon',
  inject: ['text', 'parent']
}
</script>

注意:provideinject 绑定并不是可响应的

provideinject实现数据响应式的三种方式

  • provide传递祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件实例的属性。缺点是实例上挂载了很多不需要的属性
// 祖先组件
...
provide () {
  return {
    text: this
  }
}

// 子孙组件
<template>
  <div>{{text.content}}</div>
</template>
<script>
export default {
  ...
  inject: ['text']
}
</script>
  • 使用Vue.observable 优化响应式 provide
// 祖先组件
<template>
  <div>
    <parent />
    <button @click="toChange">change</button>
  </div>
</template>

<script>
import parent from './parent'
import Vue from 'vue'

export default {
  ...
  provide () {
    this.text = Vue.observable({val: this.content})
    return {
      text: this.text
    }
  },
  methods: {
    toChange() {
      this.text.val = 'provide inject change'
    }
  }
}
</script>

// 父组件
<template>
  <div>root: {{text.val}}</div>
</template>

<script>
export default {
  ...
  inject: ['text']
}
</script>
  • provide里返回一个函数,获取组件的动态数据
// 祖先组件
<template>
  <div>
    <parent />
    <button @click="toChange">change</button>
  </div>
</template>

<script>
import parent from './parent'

export default {
  ...
  provide () {
    return {
      newText: () => this.content
    }
  },
  methods: {
    toChange() {
      this.content = 'provide inject change'
    }
  }
}
</script>

// 父组件
<template>
  <div>nText: {{nText}}</div>
</template>

<script>
export default {
  ...
  inject: ['newText'],
  computed: {
    nText () {
      return this.newText()
    }
  }
}
</script>

4、ref

  • 如果用在普通的 DOM 元素上,引用指向的就是 DOM 元素
  • 如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据

父组件

<template>
  <div class="box">
    <son ref="son" />
    <button @click="toClick">click</button>
  </div>
</template>
<script>
import son  from './son'
export default {
  name: 'RefParent',
  components: {son},
  methods: {
    toClick() {
      const $son = this.$refs.son
      console.log($son.text) // ref son
      $son.toChange()
      console.log($son.text) // ref son change
    }
  }
}
</script>

子组件 son.vue

<template>
  <div class="txt">{{text}}</div>
</template>
<script>
export default {
  name: 'RefSon',
  data() {
    return {
      text: 'ref son'
    }
  },
  methods: {
    toChange () {
      this.text = 'ref son change'
    }
  }
}
</script>

5、eventBus 中央事件总线

通过创建一个空的Vue实例作为中央事件总线(事件中心),用$emit/$on来触发事件和监听事件,实现父子兄弟跨级组件间的通信。

// main.js
Vue.prototype.$bus = new Vue()

父组件

<template>
  <div>
    <div class="root">{{message}}</div>
    <son-a />
    <son-b />
  </div>
</template>

<script>
import sonA from './son_a'
import sonB from './son_b'
export default {
  name: 'EventBus',
  components: {sonA, sonB},
  data() {
    return {
      message: ''
    }
  },
  mounted() {
    this.$bus.$on('change', data => this.message = data)
  }
}
</script>

子组件A son_a.vue

<template>
  <button @click="changeMessage">change</button>
</template>

<script>
export default {
  name: 'EventBusSonA',
  methods: {
    changeMessage() {
      this.$bus.$emit('change', 'message is change')
    }
  }
}
</script>

子组件B son_b.vue

<template>
  <div class="box">
    <div class="son">{{message}}</div>
    <grandson />
  </div>
</template>

<script>
import grandson from './grandson'
export default {
  name: 'EventBusSonB',
  components: {grandson},
  data() {
    return {
      message: ''
    }
  },
  mounted() {
    this.$bus.$on('change', data => this.message = data)
  }
}
</script>

孙组件 grandson.vue

<template>
  <div class="grandson">{{message}}</div>
</template>

<script>
export default {
  name: 'EventBusGrandson',
  data() {
    return {
      message: ''
    }
  },
  mounted() {
    this.$bus.$on('change', data => this.message = data)
  }
}
</script>

注意: 当项目较大,使用eventBus容易造成难以维护的灾难

6、v-slot

  • 父组件中通过在引用的子组件标签内添加template标签,并用v-slot提供具名插槽,将父组件中的值传递给子组件。
  • 子组件中通过同名插槽接收值。

父组件

<template>
  <son>
    <template v-slot:child>{{message}}</template>
  </son>
</template>

<script>
import son from './son'
export default {
  name: 'VSlot',
  components: {son},
  data() {
    return {
      message: 'v-slot'
    }
  }
}
</script>

子组件 son.vue

<template>
  <div class="son">
    <slot name="child"></slot>
  </div>
</template>

注意: v-slottemplate 标签中用于提供具名插槽或需要接收 prop 的插槽,如果不指定 v-slot ,则取默认值 default。

7、v-model

  • v-model作用于表单inputtextareaselect元素上,可创建双向数据绑定。
  • v-model本质上是语法糖,负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理。

v-model双向绑定原理:
a、v-bind绑定value属性的值;
b、v-on绑定input事件到监听函数中,监听函数获取最新的值并赋值到绑定的属性中。

<input v-model="inputValue" />

// 等价于
<input :value="inputValue" @input="inputValue = $event.target.value" />

v-model进行组件通信的原理:
a、 父组件通过v-model向子组件传递值,子组件通过props接收value字段,将它展示到子组件自己的input上。
b、 当子组件自身发生改变时,触发自身的input方法,然后触发父组件的事件方法,改变父组件的value,进而改变接收的props,实现自身展示的改变。
   即子组件值改变时,触发表单元素事件,并通过$emit向父组件传递事件和值。

父组件

<template>
  <div>
    <son v-model="inputValue" />
    {{inputValue}}
  </div>
</template>

<script>
import son from './son'
export default {
  name: 'VModel',
  components: {son},
  data() {
    return {
      inputValue: 'v-model'
    }
  }
}
</script>

子组件 son.vue

<template>
  <div>
    <input type="text" :value="value" @input="$emit('input', $event.target.value)" />
    {{value}}
  </div>
</template>

<script>
export default {
  name: 'VModelSon',
  model: {
    prop: 'value',
    event: 'input'
  },
  props: ['value']
}
</script>

注意:

  • 子组件中model属性的prop值与表单元素的值有关,如为单选框、复选框时,prop值为checked
  • 子组件中model属性的event值为子组件要更新父组件值时需要注册的方法

8、.sync修饰符

  • 在父组件中通过添加.sync修饰符绑定一个属性名,并将值传递给子组件
  • 在子组件中使用props接收父组件传递的值,并通过this.$emit('update:属性名', value)的形式向父组件传递值

父组件

<template>
  <div>
    <son :message.sync="mess" />
    {{mess}}
  </div>
</template>

<script>
import son from './son'
export default {
  name: 'Sync',
  components: {son},
  data() {
    return {
      mess: 'sync'
    }
  }
}
</script>

子组件 son.vue

<template>
  <button @click="$emit('update:message', 'sync had change')">change</button>
</template>

<script>
export default {
  name: 'SyncSon',
  props: ['mess']
}
</script>

9、Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

初始化Vuex模块

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

Vue.use(Vuex)

const state = {
  testA: 'default a',
  testB: 'default b'
}

const mutations = {
  receiveTestA(state, payload) {
    state.testA = payload.testA
  },
  receiveTestB(state, payload) {
    state.testB = payload.testB
  }
}

export default new Vuex.Store({
  state,
  mutations
})

父组件

<template>
  <div>
    <son-a />
    <son-b />
  </div>
</template>

<script>
import sonA from './son_a'
import sonB from './son_b'
export default {
  name: 'Vuex',
  components: {sonA, sonB}
}
</script>

子组件A son_a.vue

<template>
  <div>
    <p>testA:{{testA}}</p>
    <p>testB:{{testB}}</p>
    <button @click="ChangeA">changeA</button>
  </div>
</template>

<script>
import $store from './store' 
export default {
  name: 'VuexSonA',
  computed: {
    testA() {
      return $store.state.testA
    },
    testB() {
      return $store.state.testB
    }
  },
  methods: {
    ChangeA () {
      $store.commit('receiveTestA', {
        testA: 'testA has change'
      })
    }
  }
}
</script>

子组件B son_b.vue

<template>
  <div>
    <p>testA:{{testA}}</p>
    <p>testB:{{testB}}</p>
    <button @click="ChangeB">changeB</button>
  </div>
</template>

<script>
import $store from './store' 
export default {
  name: 'VuexSonB',
  computed: {
    testA() {
      return $store.state.testA
    },
    testB() {
      return $store.state.testB
    }
  },
  methods: {
    ChangeB () {
      $store.commit('receiveTestB', {
        testB: 'testB has change'
      })
    },
  }
}
</script>

10、$attrs/$listeners

用在父组件传递数据给子组件或者孙组件

$attrs

  • 继承所有的父组件属性(除了prop传递的属性、classstyle
  • 当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

$listeners

  • 是一个对象,包含了父作用域中的v-on事件监听器,可以配合v-on="$listeners"将所有的事件监听器指向这个组件的某个特定的子元素

父组件 parent.vue

<template>
  <div>
    <son :message="mess" @test="receive" />
  </div>
</template>

<script>
import son from './son'
export default {
  name: 'AttrsListeners',
  components: {son},
  data() {
    return {
      mess: 'parent'
    }
  },
  methods: {
    receive(data) {
      this.mess = data
    }
  }
}
</script>

子组件 son.vue

<template>
  <div>
    {{$attrs.message}}
    <grandson v-bind="$attrs" v-on="$listeners" />
  </div>
</template>

<script>
import grandson from './grandson'
export default {
  name: 'AttrsListenersSon',
  components: {grandson}
}
</script>

孙组件 grandson.vue

<template>
  <div>
    <p>{{$attrs.message}}</p>
    <button @click="Change">change</button>
  </div>
</template>

<script>
export default {
  name: 'AttrsListenersGrandson',
  methods: {
    Change () {
      this.$emit('test', 'grandson change parent')
      // 上级组件中v-on绑定了$listeners,此处可跨级触发parent组件中的test事件
    }
  }
}
</script>

如遇到表单事件时,下级组件触发上级组件事件可省略,如下:

<!--  父组件 parent.vue  -->
<template>
  <div>
    <p>parent's mess: {{mess}}</p>
    <input type="text" v-model="mess">
    <son :message="mess" @keyup="receive" />
  </div>
</template>

<script>
import son from './son'
export default {
  name: 'AttrsListeners',
  components: {son},
  data() {
    return {
      mess: 'parent'
    }
  },
  methods: {
    receive(e) {
      this.mess = e.target.value
    }
  }
}
</script>


<!--  子组件 son.vue  -->
<template>
  <div>
    <p>son's mess: {{$attrs.message}}</p>
    <input type="text" v-model="message" v-on="$listeners" />
    <grandson v-bind="$attrs" v-on="$listeners" />
  </div>
</template>

<script>
import grandson from './grandson'
export default {
  name: 'AttrsListenersSon',
  components: {grandson},
  data() {
    return {
      message: 'son'
    }
  }
}
</script>


<!--  孙组件 grandson.vue  -->
<template>
  <div>
    <p>grandson's mess: {{$attrs.message}}</p>
    <input type="text" v-model="message" v-on="$listeners" />
  </div>
</template>

<script>
export default {
  name: 'AttrsListenersGrandson',
  data() {
    return {
      message: 'grandson'
    }
  }
}
</script>

11、localStorage/sessionStorage

localStoragesessionStorage用法相同,在Vue组件通信过程中,当值改变时,需要通过事件触发获取并更新,无法同步更新。因此不推荐使用。