Vue 元件間的通訊方式

PHP定製開發發表於2022-09-28

前言

在使用crmeb的 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 實現的  dispatch 和  broadcast

attrs/attrs/ a t t r s /listeners

Vue 2.4.0 版本新增了  $attrs 和  $listeners 兩個方法。先看下官方對  $attrs 的介紹:

包含了父作用域中不作為 prop 被識別 (且獲取) 的 attribute 繫結 ( class 和  style 除外)。當一個元件沒有宣告任何 prop 時,這裡會包含所有父作用域的繫結 ( class 和  style 除外),並且可以透過  v-bind="$attrs" 傳入內部元件 —— 在建立高階別的元件時非常有用。

看個例子:

元件 A (ComponentA):

<template>
  <component-a name="Lin" age="24" sex="male"></component-a></template><script>import ComponentB from '@/components/ComponentB.vue'export default {  name: 'App',  components: {    ComponentA
  }
}</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 結構中可以看到是不會繼承父元件傳遞過來的屬性:

Vue 元件間的通訊方式

如果不加上  inheritAttrs=false 屬性,就會自動繼承父元件傳遞過來的屬性:

Vue 元件間的通訊方式

再看下  $listeners 的定義:

包含了父作用域中的 (不含  .native 修飾器的)  v-on 事件監聽器。它可以透過  v-> 傳入內部元件 —— 在建立更高層次的元件時非常有用。

$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 注入之後可以直接訪問到。

但是需要注意的是, provide 和  inject 繫結並不是可響應的,按照官方的說法, 這是刻意為之的

也就是說父元件 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 方法來實現  provide 和  inject 繫結並可響應。

基於上面的示例,改造祖先元件 (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 方法實現  provide 和  inject 的可響應更加簡單高效,推薦大家使用這種方式。

基於  $parent/ $children 實現的  dispatch 和  broadcast

先了解下 dispatch 和 broadcast 兩個概念。

  • dispatch: 派發,指的是從一個元件內部向上傳遞一個事件,並在元件內部透過  $on 進行監聽

  • broadcast: 廣播,指的是從一個元件內部向下傳遞一個事件,並在元件內部透過  $on 進行監聽

在實現 dispatch 和 broadcast 方法之前,先來看一下具體的使用方法。有  ComponentA.vue 和  ComponentB.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]))
        }
      })
    }
  }
}


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70021881/viewspace-2916646/,如需轉載,請註明出處,否則將追究法律責任。

相關文章