淺談八種 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 接收父組件數(shù)據(jù)。

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

<script>
export default {
  name: 'Son1',
  props: {
    parentNum: { // 接收父組件數(shù)據(jù)
      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 觸發(fā)父組件綁定在自己身上的事件,調(diào)用父組件相應(yīng)方法,由父組件自行修改數(shù)據(jù)(遵循單向數(shù)據(jù)流)。

// Son1.vue
<template>
  <div>
    子組件1: {{parentNum}}
    <button @click="change">修改父組件數(shù)據(jù)</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>

同樣,子組件觸發(fā)自身 changeNum:xxx 事件,調(diào)用父組件方法修改數(shù)據(jù)。

// Son1.vue
<template>
  <div>
    子組件1: {{parentNum}}
    <button @click="change">修改父組件數(shù)據(jù)</button>
  </div>
</template>

<script>
export default {
  name: 'Son1',
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('changeNum:xxx', 200) // 修改調(diào)用方法
    }
  }
}
</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>

同理,子組件調(diào)用方法改為 update:parentNum

// Son1.vue
<template>
  <div>
    子組件1: {{parentNum}}
    <button @click="change">修改父組件數(shù)據(jù)</button>
  </div>
</template>

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

語法糖 .sync 誕生:同步父子組件數(shù)據(jù)。

父組件使用 :parentNum.sync="num" 語法糖簡寫,省略 @update:parentNum="change"

因為語法糖 :parentNum.sync="num"

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

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

的簡寫,已經(jīng)包含數(shù)據(jù)修改方法 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>

子組件同上改進,觸發(fā) update:parentNum 事件,調(diào)用父組件方法修改數(shù)據(jù)。

注意,使用語法糖 .sync 時,子組件必須使用 update

// Son1.vue
<template>
  <div>
    子組件1: {{parentNum}}
    <button @click="change">修改父組件數(shù)據(jù)</button>
  </div>
</template>

<script>
export default {
  name: 'Son1',
  props: {
    parentNum: {
      type: Number,
      default: 0
    }
  },
  methods: {
    change () {
      this.$emit('update:parentNum', 200) // 修改調(diào)用方法
    }
  }
}
</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,觸發(fā)方法使用 input

<template>
  <div>
    子組件1: {{value}}
    <button @click="change">修改父組件數(shù)據(jù)</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"

的簡寫,已經(jīng)包含數(shù)據(jù)修改方法 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,觸發(fā)回調(diào)事件名只能是input

// Son1.vue
<template>
  <div>
    子組件1: {{value}}
    <button @click="change">修改父組件數(shù)據(jù)</button>
  </div>
</template>

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

1.3 parentchildren

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

父組件數(shù)據(jù)傳遞到子組件,將事件 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>

子組件接收數(shù)據(jù),并將數(shù)據(jù)傳遞到孫組件。

// Son1.vue
<template>
  <div>
    子組件1: {{parentNum}}
    <button @click="change">修改父組件數(shù)據(jù)</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>

孫組件接收數(shù)據(jù),并通過 $parent 調(diào)用其父組件(即子組件),由于子組件上綁定有爺組件(即父組件)傳遞的方法 changeNum,故可進行調(diào)用修改數(shù)據(jù)。

// Grandson1.vue
<template>
  <div>
    孫組件1:{{parentNum}}
    <button @click="change">修改爺組件數(shù)據(jù)</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 向上派發(fā)

如上,如果層級很深,那么就會出現(xiàn) $parent.$parent..... 的情況,可以封裝一個 $dispatch 方法向上進行派發(fā)。

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

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

// 向上派發(fā)
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')

后代組件調(diào)用原型上的 $dispatch 方法。

// Grandson1.vue
<template>
  <div>
    孫組件1:{{parentNum}}
    <button @click="change">修改爺組件數(shù)據(jù)</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 向下通知

同理,如果層級很深,調(diào)用后代方法就會出現(xiàn)多個 $children 循環(huán)遍歷的情況,可以封裝一個 $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')

為長輩組件添加調(diào)用后代組件方法的方法。

// Parent.vue
<template>
  <div>
    父組件: {{num}} 
    <button @click="triggerChild">觸發(fā)后代方法</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') // 調(diào)用后代中的 changeChild 方法
    }
  }
}
</script>

在后代組件中綁定供長輩組件調(diào)用的方法。

// Son1.vue
<template>
  <div>
    子組件1: {{parentNum}}
    <button @click="change">修改父組件數(shù)據(jù)</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 統(tǒng)一接收,并傳遞給后代組件傳遞。

接收的 $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>

孫組件調(diào)用 $listeners 中的某個方法。

// Grandson1.vue
<template>
  <div>
    孫組件1:<button @click="change">調(diào)用前輩方法</button>
  </div>
</template>

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

1.5 provide inject

如上,后代使用前輩方法需要依次傳遞,但希望能夠?qū)崿F(xiàn)前輩組件定義方法,后代均可直接共享(類似于 react 中的 context)。

后代組件可以注入數(shù)據(jù),也可以注入前輩組件(前輩組件先提供)。

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">調(diào)用前輩方法</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 節(jié)點。放在組件上,獲取的是當(dāng)前組件。故可以使用 ref 調(diào)用子組件屬性與方法。

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>

父組件調(diào)用子組件屬性與方法。

// Parent.vue
<template>
  <div>
    父組件 {{num}}
    <button @click="useChild">調(diào)用子組件方法</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 // 調(diào)用子組件屬性
  },
  methods: {
    useChild () {
      this.$refs.Son1.changeNum(201) // 調(diào)用子組件方法
    }
  }
}
</script>
1.6-1.gif

1.7 EventBus

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

EventBus 通常用于兄弟組件及其他非間隔組件之間的數(shù)據(jù)通信。

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

為什么使用 vue 實例?因為 vue 實例上有 $on$emit 方法,可用于收集和觸發(fā)事件。

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被觸發(fā)")
    })
  },
}
</script>

子組件2在 $bus 上綁定事件。

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

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

孫組件1觸發(fā) $bus 上綁定的指定事件。

![8](../../8.gif<template>
  <div>
    孫組件1:<button @click="change">調(diào)用方法</button>
  </div>
</template>

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

如上,此 EventBus 方法缺陷是如果綁定了相同的事件名,在一次觸發(fā)時所有相同事件名事件都將被觸發(fā)。

1.8 Vuex

待添加。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。