浅谈八种 Vue 组件通信方式

1.1 Props

components
   ├── Parent.vue   // 父
   └── Son1.vue     // 子1

父组件中使用子组件。

// Parent.vue
<template>
  <div>
    父组件: {{num}}
    <Son1 :parentNum="num"></Son1>
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  }
}
</script>

子组件通过 props 接收父组件数据。

// Son1.vue
<template>
  <div>
    子组件1: {{parentNum}}
  </div>
</template>

<script>
export default {
  name: 'Son1',
  props: {
    parentNum: { // 接收父组件数据
      type: Number,
      default: 0
    }
  }
}
</script>
1.1-1.png

1.2 $emit 使用

components
   ├── Parent.vue   // 父
   └── Son1.vue     // 子1

父组件通过 v-on 将事件绑定在子组件身上。

// Parent.vue
<template>
  <div>
    父组件: {{num}}
    <Son1 :parentNum="num" @changeNum="change"></Son1>
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  },
  methods: {
    change (newNum) {
      this.num = newNum
    }
  }
}
</script>

子组件通过 $emit 触发父组件绑定在自己身上的事件,调用父组件相应方法,由父组件自行修改数据(遵循单向数据流)。

// Son1.vue
<template>
  <div>
    子组件1: {{parentNum}}
    <button @click="change">修改父组件数据</button>
  </div>
</template>

<script>
export default {
  name: 'Son1',
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('changeNum', 200)
    }
  }
}
</script>
1.2-1.gif
1.2.1 .sync 语法糖

改写:对以上绑定事件方法进行改写。

父组件向子组件传递值与方法,此时绑定的事件为 changeNum:xxx

// Parent.vue
<template>
  <div>
    父组件: {{num}}
    <Son1 :parentNum="num" @changeNum:xxx="change"></Son1>
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  },
  methods: {
    change (newNum) {
      this.num = newNum
    }
  }
}
</script>

同样,子组件触发自身 changeNum:xxx 事件,调用父组件方法修改数据。

// Son1.vue
<template>
  <div>
    子组件1: {{parentNum}}
    <button @click="change">修改父组件数据</button>
  </div>
</template>

<script>
export default {
  name: 'Son1',
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('changeNum:xxx', 200) // 修改调用方法
    }
  }
}
</script>
1.2.1-1.gif

进一步改写:将事件 changeNum:xxx 改写为 update:parentNum

// Parent.vue
<template>
  <div>
    父组件: {{num}}
    <Son1 :parentNum="num" @update:parentNum="change"></Son1>
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  },
  methods: {
    change (newNum) {
      this.num = newNum
    }
  }
}
</script>

同理,子组件调用方法改为 update:parentNum

// Son1.vue
<template>
  <div>
    子组件1: {{parentNum}}
    <button @click="change">修改父组件数据</button>
  </div>
</template>

<script>
export default {
  name: 'Son1',
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('update:parentNum', 200) // 修改调用方法
    }
  }
}
</script>
1.2.1-2.gif

语法糖 .sync 诞生:同步父子组件数据。

父组件使用 :parentNum.sync="num" 语法糖简写,省略 @update:parentNum="change"

因为语法糖 :parentNum.sync="num"

:parentNum="num" @update:parentNum="change"

:parentNum="num" @update:parentNum="newNum => num = newNum"

的简写,已经包含数据修改方法 newNum => num = newNum,故不再需要 change 事件。

// Parent.vue
<template>
  <div>
    父组件: {{num}}
    <Son1 :parentNum.sync="num"></Son1>
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  }
}
</script>

子组件同上改进,触发 update:parentNum 事件,调用父组件方法修改数据。

注意,使用语法糖 .sync 时,子组件必须使用 update

// Son1.vue
<template>
  <div>
    子组件1: {{parentNum}}
    <button @click="change">修改父组件数据</button>
  </div>
</template>

<script>
export default {
  name: 'Son1',
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('update:parentNum', 200) // 修改调用方法
    }
  }
}
</script>
1.2.1-3.gif
1.2.2 v-model 语法糖

同上改写,此时 父组件传值绑定为 value,方法修改为 input

// Parent.vue
<template>
  <div>
    父组件: {{num}}
    <Son1 :value="num" @input="change"></Son1>
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  },
  methods: {
    change (newNum) {
      this.num = newNum
    }
  }
}
</script>

此时子组件值使用 value,触发方法使用 input

<template>
  <div>
    子组件1: {{value}}
    <button @click="change">修改父组件数据</button>
  </div>
</template>

<script>
export default {
  name: 'Son1',
  props: {
    value: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('input', 200)
    }
  }
}
</script>
1.2.2-1.gif

语法糖 v-model 诞生

父组件使用 v-model="num" 语法糖简写,省略 :value="num" @input="change"

因为语法糖 v-model="num"

:value="num" @input="change"

:value="num" @input="newNum => num = newNum"

的简写,已经包含数据修改方法 newNum => num = newNum,故不再需要 change 事件。

// Parent.vue
<template>
  <div>
    父组件: {{num}}
    <Son1 v-model="num" />
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  }
}
</script>

同上,子组件接收属性名只能是 value,触发回调事件名只能是input

// Son1.vue
<template>
  <div>
    子组件1: {{value}}
    <button @click="change">修改父组件数据</button>
  </div>
</template>

<script>
export default {
  name: 'Son1',
  props: {
    value: { // 接收属性名只能是 value
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('input', 200) // 触发事件只能是 input
    }
  }
}
</script>
1.2.2-2.gif

1.3 parentchildren

components
   ├── Parent.vue   // 父
   ├── Son1.vue     // 子1
   └── GrandSon.vue // 孙1

父组件数据传递到子组件,将事件 changeNum 绑定到子组件上。

// Parent.vue
<template>
  <div>
    父组件: {{num}}
    <Son1 :parentNum="num" @changeNum="change"></Son1>
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  },
  methods: {
    change (newNum) {
      this.num = newNum
    }
  }
}
</script>

子组件接收数据,并将数据传递到孙组件。

// Son1.vue
<template>
  <div>
    子组件1: {{parentNum}}
    <button @click="change">修改父组件数据</button>
    <br />
    <Grandson1 :parentNum="parentNum" />
  </div>
</template>

<script>
import Grandson1 from './Grandson1'

export default {
  name: 'Son1',
  components: {
    Grandson1
  },
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('changeNum', 200)
    }
  }
}
</script>

孙组件接收数据,并通过 $parent 调用其父组件(即子组件),由于子组件上绑定有爷组件(即父组件)传递的方法 changeNum,故可进行调用修改数据。

// Grandson1.vue
<template>
  <div>
    孙组件1:{{parentNum}}
    <button @click="change">修改爷组件数据</button>
  </div>
</template>

<script>
export default {
  name: 'Grandson1',
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$parent.$emit('changeNum', 300)
    }
  },
}
</script>
1.3-1.gif
1.3.1 $dispatch 向上派发

如上,如果层级很深,那么就会出现 $parent.$parent..... 的情况,可以封装一个 $dispatch 方法向上进行派发。

main.js 中 为 Vue 原型添加 $dispatch 方法,以便每个 vue 实例均可使用。

// main.js
import Vue from 'vue'
import App from './App.vue'

// 向上派发
Vue.prototype.$dispatch = function (eventName, value) {
  let parent = this.$parent
  while (parent) {
    parent.$emit(eventName, value)
    parent = parent.$parent
  }
}

new Vue({
  render: h => h(App)
}).$mount('#app')

后代组件调用原型上的 $dispatch 方法。

// Grandson1.vue
<template>
  <div>
    孙组件1:{{parentNum}}
    <button @click="change">修改爷组件数据</button>
  </div>
</template>

<script>
export default {
  name: 'Grandson1',
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$dispatch('changeNum', 300)
    }
  },
}
</script>
1.3.1-1.gif
1.3.2 $broadcast 向下通知

同理,如果层级很深,调用后代方法就会出现多个 $children 循环遍历的情况,可以封装一个 $broadcast 方法向下进行通知。

main.js 中 为 Vue 原型添加 $dispatch 方法,以便每个 vue 实例均可使用。

// main.js
import Vue from 'vue'
import App from './App.vue'

// 向下通知
Vue.prototype.$broadcast = function (eventName, value) {
  const broadcast = children => {
    children.forEach(child => {
      child.$emit(eventName, value)
      if (child.$children) broadcast(child.$children)
    })
  }

  broadcast(this.$children)
}

new Vue({
  render: h => h(App)
}).$mount('#app')

为长辈组件添加调用后代组件方法的方法。

// Parent.vue
<template>
  <div>
    父组件: {{num}} 
    <button @click="triggerChild">触发后代方法</button>
    <Son1 :parentNum="num" @changeNum="change"></Son1>
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  },
  methods: {
    change (newNum) {
      this.num = newNum
    },
    triggerChild () {
      this.$broadcast('changeChild') // 调用后代中的 changeChild 方法
    }
  }
}
</script>

在后代组件中绑定供长辈组件调用的方法。

// Son1.vue
<template>
  <div>
    子组件1: {{parentNum}}
    <button @click="change">修改父组件数据</button>
    <br />
    <!-- 绑定方法 --> 
    <Grandson1 :parentNum="parentNum" @changeChild="change" />
  </div>
</template>

<script>
import Grandson1 from './Grandson1'

export default {
  name: 'Son1',
  components: {
    Grandson1
  },
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('changeNum', 200)
    }
  }
}
</script>
1.3.2-1.gif

1.4 attrslisteners

components
   ├── Parent.vue   // 父
   ├── Son1.vue     // 子1
   └── GrandSon.vue // 孙1
1.4.1 $attrrs 属性集合

父组件定义多个属性,传递给孙组件。

需先由父组件传递给子组件。

<template>
  <div>
    父组件
    <Son1 :name="name" :age=" 12" :sex="sex" />
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      name: '张三',
      age: '12',
      sex: '男'
    }
  }
}
</script>

子组件可以使用 props 依次接收,然后在传递给孙组件。但是,每个组件向下传递时都写在 props 中很麻烦,所以,可以使用 $attrs 统一接收,并传递给后代组件传递。

接收的 $attrs 是属性对象,可以传递单个属性,也可使用 v-bind="{name: $attrs.name, age: '$attrs.age'}" 传递部分属性,也可以使用 v-bind="$attrs" 传递全部属性。

<template>
  <div>
    子组件1: {{$attrs}} 
    <!-- <Grandson1 :name="$attrs.name" :age="$attrs.age" :sex="$attrs.sex" /> -->
    <!-- <Grandson1 v-bind="{name: $attrs.name, age: '$attrs.age'}" /> -->
    <Grandson1 v-bind='$attrs' />
  </div>
</template>

<script>
import Grandson1 from './Grandson1'

export default {
  name: 'Son1',
  components: {
    Grandson1
  }
}
</script>

孙组件使用父组件传递的属性。

//Grandson1.vue
<template>
  <div>
    孙组件1:姓名:{{$attrs.name}} 年龄:{{$attrs.age}} 性别:{{$attrs.sex}} 
  </div>
</template>

<script>
export default {
  name: 'Grandson1'
}
</script>
1.4.1-1.png

此时可以看到,传递的属性同样添加到了 DOM 属性上。

1.4.1-2.png

若不想将属性添加到 DOM 上,可声明不继承。

//Grandson1.vue
<template>
  <div>
    孙组件1:姓名:{{$attrs.name}} 年龄:{{$attrs.age}} 性别:{{$attrs.sex}} 
  </div>
</template>

<script>
export default {
  name: 'Grandson1',
  inheritAttrs: false // 声明不继承
}
</script>
1.4.1-3.png
1.4.2 $listeners 方法集合

父组件定义多个属性,传递给孙组件。

需先由父组件传递给子组件。

// Parent.vue
<template>
  <div>
    父组件 {{num}}
    <Son1 @changeNum="change" />
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  },
  methods: {
    change (newNum) {
      this.num = newNum
    }
  }
}
</script>

子组件使用 $listener 接收父组件传入的全部方法,并使用 v-on 传递给孙组件。

// Son1.vue
<template>
  <div>
    子组件1
    <Grandson1 v-on='$listeners' />
  </div>
</template>

<script>
import Grandson1 from './Grandson1'

export default {
  name: 'Son1',
  components: {
    Grandson1
  }
}
</script>

孙组件调用 $listeners 中的某个方法。

// Grandson1.vue
<template>
  <div>
    孙组件1:<button @click="change">调用前辈方法</button>
  </div>
</template>

<script>
export default {
  name: 'Grandson1',
  methods: {
    change () {
      this.$listeners.changeNum(300)
    }
  }
}
</script>
1.4.2-1.gif

1.5 provide inject

如上,后代使用前辈方法需要依次传递,但希望能够实现前辈组件定义方法,后代均可直接共享(类似于 react 中的 context)。

后代组件可以注入数据,也可以注入前辈组件(前辈组件先提供)。

components
   ├── Parent.vue   // 父
   ├── Son1.vue     // 子1
   └── GrandSon.vue // 孙1
1.5.1 Provide

父组件使用 provide 提供自身。

// Parent.vue
<template>
  <div>
    父组件 {{num}}
    <Son1 />
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  provide () {
    return {
      parent: this // 提供自己
    }
  },
  data () {
    return {
      num: 100
    }
  },
  methods: {
    change (newNum) {
      this.num = newNum
    }
  }
}
</script>
1.5.2 inject

孙组件使用 inject 注入其爷组件(父组件)。

// Grandson1.vue
<template>
  <div>
    孙组件1:<button @click="change">调用前辈方法</button>
  </div>
</template>

<script>
export default {
  name: 'Grandson1',
  inject: [ 'parent'],
  methods: {
    change () {
      this.parent.change(300)
    }
  }
}
</script>
1.5.2-1.gif

1.6 ref

ref 放在 DOM 元素上,获取的是 DOM 节点。放在组件上,获取的是当前组件。故可以使用 ref 调用子组件属性与方法。

components
   ├── Parent.vue   // 父
   └── Son1.vue     // 子1

子组件定义属性与方法。

// Son1.vue
<template>
  <div>
    子组件1:{{num}}
  </div>
</template>

<script>
export default {
  name: 'Son1',
  data () {
    return {
      num: 200
    }
  },
  methods: {
    changeNum (newNum) {
      this.num = newNum
    }
  }
}
</script>

父组件调用子组件属性与方法。

// Parent.vue
<template>
  <div>
    父组件 {{num}}
    <button @click="useChild">调用子组件方法</button>
    <Son1 ref="Son1" />
  </div>
</template>

<script>
import Son1 from "./Son1"

export default {
  name: 'Parent',
  components: {
    Son1
  },
  data () {
    return {
      num: 100
    }
  },
  mounted () {
    this.num = this.$refs.Son1.num // 调用子组件属性
  },
  methods: {
    useChild () {
      this.$refs.Son1.changeNum(201) // 调用子组件方法
    }
  }
}
</script>
1.6-1.gif

1.7 EventBus

components
   ├── Parent.vue      // 父
   ├── Son1.vue        // 子1
   ├── Grandson1.vue   // 子1
   └── Son2.vue        // 子2

EventBus 通常用于兄弟组件及其他非间隔组件之间的数据通信。

main.js 中在 Vue 原型上添加 $bus,其是一个 vue 实例。

为什么使用 vue 实例?因为 vue 实例上有 $on$emit 方法,可用于收集和触发事件。

import Vue from 'vue'
import App from './App.vue'

Vue.prototype.$bus = new Vue()

new Vue({
  render: h => h(App)
}).$mount('#app')

父组件引入两个子组件。

// Parent.vue
<template>
  <div>
    父组件
    <Son1 />
    <Son2 />
  </div>
</template>

<script>
import Son1 from "./Son1"
import Son2 from "./Son2"

export default {
  name: 'Parent',
  components: {
    Son1,
    Son2
  }
}
</script>

子组件1在 $bus 上绑定事件。

// Son1.vue
<template>
  <div>
    子组件1
    <Grandson1 />
  </div>
</template>

<script>
import Grandson1 from './Grandson1'

export default {
  name: 'Son1',
  components: {
    Grandson1
  },
  mounted () {
    this.$bus.$on('change', () =>{
      console.log("Son1被触发")
    })
  },
}
</script>

子组件2在 $bus 上绑定事件。

// Son2.vue
<template>
  <div>
    子组件2
  </div>
</template>

<script>
export default {
  name: 'Son2',
  mounted () {
    this.$bus.$on('change', () =>{
      console.log("Son2被触发")
    })
  },
}
</script>

孙组件1触发 $bus 上绑定的指定事件。

![8](../../8.gif<template>
  <div>
    孙组件1:<button @click="change">调用方法</button>
  </div>
</template>

<script>
export default {
  name: 'Grandson1',
  methods: {
    change () {
      this.$bus.$emit('change')
    }
  }
}
</script>
1.7-1.gif

如上,此 EventBus 方法缺陷是如果绑定了相同的事件名,在一次触发时所有相同事件名事件都将被触发。

1.8 Vuex

待添加。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335