Vue 元件通訊方式全面詳解

littleLane發表於2019-02-28

眾所周知,Vue 主要思想之一就是元件式開發。因此,在實際的專案開發中,肯定會以元件的開發模式進行。形如頁面和頁面之間需要通訊一樣,Vue 元件和元件之間肯定也需要互通有無、共享狀態。接下來,我們就悉數給大家展示所有 Vue 元件之間的通訊方式。

元件關係

Vue 元件通訊.png


上面展示的圖片可以引入所有 Vue 元件的關係形式:

  • A 元件和 B 元件、B 元件和 C 元件、B 元件和 D 元件形成了父子關係
  • C 元件和 D 元件形成了兄弟關係
  • A 元件和 C 元件、A 元件和 D 元件形成了隔代關係(其中的層級可能是多級,即隔多代)

元件通訊

這麼多的元件關係,那麼元件和元件之間又有哪些通訊的方式呢?各種方式的區別又是什麼?適用場景又是什麼呢?帶著問題繼續往下看吧!

1、props$emit

用過 Vue 技術棧開發專案過的開發者對這樣一個組合肯定不會陌生,這種元件通訊的方式是我們運用的非常多的一種。props 以單向資料流的形式可以很好的完成父子元件的通訊。

所謂單向資料流:就是資料只能通過 props 由父元件流向子元件,而子元件並不能通過修改 props 傳過來的資料修改父元件的相應狀態。至於為什麼這樣做,Vue 官網做出瞭解釋:

所有的 prop 都使得其父子 prop 之間形成了一個單向下行繫結:父級 prop 的更新會向下流動到子元件中,但是反過來則不行。這樣會防止從子元件意外改變父級元件的狀態,從而導致你的應用的資料流向難以理解。

額外的,每次父級元件發生更新時,子元件中所有的 prop 都將會重新整理為最新的值。這意味著你不應該在一個子元件內部改變 prop。如果你這樣做了,Vue 會在瀏覽器的控制檯中發出警告。

——Vue 官網

正因為這個特性,於是就有了對應的 $emit$emit 用來觸發當前例項上的事件。對此,我們可以在父元件自定義一個處理接受變化狀態的邏輯,然後在子元件中如若相關的狀態改變時,就觸發父元件的邏輯處理事件。

// 父元件
Vue.component('parent', {
  template:`
    <div>
      <p>this is parent component!</p>
      <child :message="message" v-on:getChildData="getChildData"></child>
    </div>
  `,
  data() {
    return {
      message: 'hello'
    }
  },
  methods:{
    // 執行子元件觸發的事件
    getChildData(val) {
      console.log(val);
    }
  }
});

// 子元件
Vue.component('child', {
  template:`
    <div>
      <input type="text" v-model="myMessage" @input="passData(myMessage)">
    </div>
  `,
  /**
   * 得到父元件傳遞過來的資料
   * 這裡的定義最好是寫成資料校驗的形式,免得得到的資料是我們意料之外的
   *
   * props: {
   *   message: {
   *     type: String,
   *     default: ''
   *   }
   * }
   *
  */
  props:['message'], 
  data() {
    return {
      // 這裡是必要的,因為你不能直接修改 props 的值
      myMessage: this.message
    }
  },
  methods:{
    passData(val) {
      // 資料狀態變化時觸發父元件中的事件
      this.$emit('getChildData', val);
    }
  }
});
    
var app=new Vue({
  el: '#app',
  template: `
    <div>
      <parent />
    </div>
  `
});
複製程式碼

在上面的例子中,有父元件 parent 和子元件 child。

  • 1)、 父元件傳遞了 message 資料給子元件,並且通過v-on繫結了一個 getChildData 事件來監聽子元件的觸發事件;
  • 2)、 子元件通過 props 得到相關的 message 資料,然後將資料快取在 data 裡面,最後當屬性資料值發生變化時,通過 this.$emit 觸發了父元件註冊的 getChildData 事件處理資料邏輯。

2、$attrs$listeners

上面這種元件通訊的方式只適合直接的父子元件,也就是如果父元件A下面有子元件B,元件B下面有元件C,這時如果元件A直接想傳遞資料給元件C那就行不通了! 只能是元件A通過 props 將資料傳給元件B,然後元件B獲取到元件A 傳遞過來的資料後再通過 props 將資料傳給元件C。當然這種方式是非常複雜的,無關元件中的邏輯業務一種增多了,程式碼維護也沒變得困難,再加上如果巢狀的層級越多邏輯也複雜,無關程式碼越多!

針對這樣一個問題,Vue 2.4 提供了$attrs$listeners 來實現能夠直接讓元件A傳遞訊息給元件C。

// 元件A
Vue.component('A', {
  template: `
    <div>
      <p>this is parent component!</p>
      <B :messagec="messagec" :message="message" v-on:getCData="getCData" v-on:getChildData="getChildData(message)"></B>
    </div>
  `,
  data() {
    return {
      message: 'hello',
      messagec: 'hello c' //傳遞給c元件的資料
    }
  },
  methods: {
    // 執行B子元件觸發的事件
    getChildData(val) {
      console.log(`這是來自B元件的資料:${val}`);
    },
    
    // 執行C子元件觸發的事件
    getCData(val) {
      console.log(`這是來自C元件的資料:${val}`);
    }
  }
});

// 元件B
Vue.component('B', {
  template: `
    <div>
      <input type="text" v-model="mymessage" @input="passData(mymessage)"> 
      <!-- C元件中能直接觸發 getCData 的原因在於:B元件呼叫 C元件時,使用 v-on 繫結了 $listeners 屬性 -->
      <!-- 通過v-bind 繫結 $attrs 屬性,C元件可以直接獲取到 A元件中傳遞下來的 props(除了 B元件中 props宣告的) -->
      <C v-bind="$attrs" v-on="$listeners"></C>
    </div>
  `,
  /**
   * 得到父元件傳遞過來的資料
   * 這裡的定義最好是寫成資料校驗的形式,免得得到的資料是我們意料之外的
   *
   * props: {
   *   message: {
   *     type: String,
   *     default: ''
   *   }
   * }
   *
  */
  props: ['message'],
  data(){
    return {
      mymessage: this.message
    }
  },
  methods: {
    passData(val){
      //觸發父元件中的事件
      this.$emit('getChildData', val)
    }
  }
});

// 元件C
Vue.component('C', {
  template: `
    <div>
      <input type="text" v-model="$attrs.messagec" @input="passCData($attrs.messagec)">
    </div>
  `,
  methods: {
    passCData(val) {
      // 觸發父元件A中的事件
      this.$emit('getCData',val)
    }
  }
});
    
var app=new Vue({
  el:'#app',
  template: `
    <div>
      <A />
    </div>
  `
});
複製程式碼

在上面的例子中,我們定義了 A,B,C 三個元件,其中元件B 是元件 A 的子元件,元件C 是元件B 的子元件。

  • 1). 在元件 A 裡面為元件 B 和元件 C 分別定義了一個屬性值(message,messagec)和監聽事件(getChildData,getCData),並將這些通過 props 傳遞給了元件 A 的直接子元件 B;
  • 2). 在元件 B 中通過 props 只獲取了與自身直接相關的屬性(message),並將屬性值快取在了 data 中,以便後續的變化監聽處理,然後當屬性值變化時觸發父元件 A 定義的資料邏輯處理事件(getChildData)。關於元件 B 的直接子元件 C,傳遞了屬性 $attrs 和繫結了事件 $listeners
  • 3). 在元件 C 中直接在 v-model 上繫結了 $attrs 屬性,通過 v-on 繫結了 $listeners

最後就將 $attrs$listeners 單獨拿出來說說吧!

  • $attrs:包含了父作用域中不被 prop 所識別 (且獲取) 的特性繫結 (classstyle 除外)。當一個元件沒有宣告任何 prop 時,這裡會包含所有父作用域的繫結屬性 (class和 style 除外),並且可以通過 v-bind="$attrs" 傳入內部元件。

  • $listeners:包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它可以通過 v-on="$listeners" 傳入內部元件。

3、中央事件匯流排 EventBus

對於父子元件之間的通訊,上面的兩種方式是完全可以實現的,但是對於兩個元件不是父子關係,那麼又該如何實現通訊呢?在專案規模不大的情況下,完全可以使用中央事件匯流排 EventBus 的方式。如果你的專案規模是大中型的,那你可以使用我們後面即將介紹的 Vuex 狀態管理

EventBus 通過新建一個 Vue 事件 bus 物件,然後通過 bus.$emit 觸發事件,bus.$on 監聽觸發的事件。

// 元件 A
Vue.component('A', {
  template: `
    <div>
      <p>this is A component!</p>
      <input type="text" v-model="mymessage" @input="passData(mymessage)"> 
    </div>
  `,
  data() {
    return {
      mymessage: 'hello brother1'
    }
  },
  methods: {
    passData(val) {
      //觸發全域性事件globalEvent
      this.$EventBus.$emit('globalEvent', val)
    }
  }
});

// 元件 B
Vue.component('B', {
  template:`
    <div>
      <p>this is B component!</p>
      <p>元件A 傳遞過來的資料:{{brothermessage}}</p>
    </div>
  `,
  data() {
    return {
      mymessage: 'hello brother2',
      brothermessage: ''
    }
  },
  mounted() {
    //繫結全域性事件globalEvent
    this.$EventBus.$on('globalEvent', (val) => {
      this.brothermessage = val;
    });
  }
});

//定義中央事件匯流排
const EventBus = new Vue();

// 將中央事件匯流排賦值到 Vue.prototype 上,這樣所有元件都能訪問到了
Vue.prototype.$EventBus = EventBus;

const app = new Vue({
  el: '#app',
  template: `
    <div>
      <A />
      <B />
    </div>
  `
});
複製程式碼

在上面的例項中,我們定義了元件 A 和元件 B,但是元件 A 和元件 B 之間沒有任何的關係。

  • 1)、 首先我們通過 new Vue() 例項化了一個 Vue 的例項,也就是我們這裡稱呼的中央事件匯流排 EventBus ,然後將其賦值給了 Vue.prototype.$EventBus,使得所有的業務邏輯元件都能夠訪問到;
  • 2)、 然後定義了元件 A,在元件 A 裡面定義了一個處理的方法 passData,主要定義觸發一個全域性的 globalEvent 事件,並傳遞一個引數;
  • 3). 最後定義了元件 B,在元件 B 裡面的 mounted 生命週期監聽了元件 A 裡面定義的全域性 globalEvent 事件,並在回撥函式裡面執行了一些邏輯處理。

中央事件匯流排 EventBus 非常簡單,就是任意元件和元件之間打交道,沒有多餘的業務邏輯,只需要在狀態變化元件觸發一個事件,然後在處理邏輯元件監聽該事件就可以。該方法非常適合小型的專案!

4、provideinject

熟悉 React 開發的同學對 Context API 肯定不會陌生吧!在 Vue 中也提供了類似的 API 用於元件之間的通訊。

在父元件中通過 provider 來提供屬性,然後在子元件中通過 inject 來注入變數。不論子元件有多深,只要呼叫了 inject 那麼就可以注入在 provider 中提供的資料,而不是侷限於只能從當前父元件的 prop 屬性來獲取資料,只要在父元件的生命週期內,子元件都可以呼叫。這和 React 中的 Context API 有沒有很相似!

// 定義 parent 元件
Vue.component('parent', {
  template: `
    <div>
      <p>this is parent component!</p>
      <child></child>
    </div>
  `,
  provide: {
    for:'test'
  },
  data() {
    return {
      message: 'hello'
    }
  }
});

// 定義 child 元件
Vue.component('child', {
  template: `
    <div>
      <input type="tet" v-model="mymessage"> 
    </div>
  `,
  inject: ['for'],	// 得到父元件傳遞過來的資料
  data(){
    return {
      mymessage: this.for
    }
  },
});

const app = new Vue({
  el: '#app',
  template: `
    <div>
      <parent />
    </div>
  `
});
複製程式碼

在上面的例項中,我們定義了元件 parent 和元件 child,元件 parent 和元件 child 是父子關係。

  • 1)、 在 parent 元件中,通過 provide 屬性,以物件的形式向子孫元件暴露了一些屬性
  • 2)、 在 child 元件中,通過 inject 屬性注入了 parent 元件提供的資料,實際這些通過 inject 注入的屬性是掛載到 Vue 例項上的,所以在元件內部可以通過 this 來訪問。

⚠️ 注意:官網文件提及 provide 和 inject 主要為高階外掛/元件庫提供用例,並不推薦直接用於應用程式程式碼中。

關於 provideinject 這對屬性的更多具體用法可以參照官網的文件


寫到這裡有點累了,前面大致介紹了四種 Vue 元件通訊的方式,你覺得這些就夠了嗎?不不不,講完前面四種方式後面還有四種等著我們呢! 藉此加個分割線,壓壓驚!?對了,千萬不要說學不動了,只要還有一口氣,都要繼續學!


5、v-model

這種方式和前面講到的 props 有點型別,但是既然單獨提出來說了,那肯定也有其獨特之處!不管了,先上程式碼吧!

// 定義 parent 元件
Vue.component('parent', {
  template: `
    <div>
      <p>this is parent component!</p>
      <p>{{message}}</p>
      <child v-model="message"></child>
    </div>
  `,
  data() {
    return {
      message: 'hello'
    }
  }
});

// 定義 child 元件
Vue.component('child', {
  template: `
    <div>
      <input type="text" v-model="mymessage" @change="changeValue"> 
    </div>
  `,
  props: {
    value: String, // v-model 會自動傳遞一個欄位為 value 的 props 屬性
  },
  data() {
    return {
      mymessage: this.value
    }
  },
  methods: {
    changeValue() {
      this.$emit('input', this.mymessage); //通過如此呼叫可以改變父元件上 v-model 繫結的值
    }
  },
});

const app = new Vue({
  el: '#app',
  template: `
     <div>
      <parent />
    </div>
  `
});
複製程式碼

說到 v-model 這個指定,大家肯定會想到雙向資料繫結,如 input 輸入值,下面的顯示就是實時的根據輸入的不同而顯示相應的內容。剛開始學習 Vue 的時候有沒有覺得很神奇,不管你有沒有,反正我有過這種感覺!

關於詳細的 v-model 用法和自定義元件 v-model 的實現,可以到這裡檢視!這裡我們主要講解 v-model 是如何實現父子元件通訊的。

在上面的例項程式碼中,我們定義了 parent 和 child 兩個元件,這兩個元件是父子關係,v-model 也只能實現父子元件之間的通訊。

  • 1)、 在 parent 元件中,我們給自定義的 child 元件實現了 v-model 繫結了 message 屬性。此時相當於給 child 元件傳遞了 value 屬性和繫結了 input 事件。
  • 2)、 順理成章,在定義的 child 元件中,可以通過 props 獲取 value 屬性,根據 props 單向資料流的原則,又將 value 快取在了 data 裡面的 mymessage 上,再在 input 上通過 v-model 繫結了 mymessage 屬性和一個 change 事件。當 input 值變化時,就會觸發 change 事件,處理 parent 元件通過 v-model 給 child 元件繫結的 input 事件,觸發 parent 元件中 message 屬性值的變化,完成 child 子元件改變 parent 元件的屬性值。

這裡主要是 v-model 的實現原理要著重瞭解一下!這種方式的用處適合於將展示元件和業務邏輯元件分離。

6、$parent$children

這裡要說的這種方式就比較直觀了,直接操作父子元件的例項。$parent 就是父元件的例項物件,而 $children 就是當前例項的直接子元件例項了,不過這個屬性值是陣列型別的,且並不保證順序,也不是響應式的。

// 定義 parent 元件
Vue.component('parent', {
  template: `
    <div>
      <p>this is parent component!</p>
      <button @click="changeChildValue">test</button>
      <child />
    </div>
  `,
  data() {
    return {
      message: 'hello'
    }
  },
  methods: {
    changeChildValue(){
      this.$children[0].mymessage = 'hello';
    }
  },
});

// 定義 child 元件
Vue.component('child', {
  template:`
    <div>
      <input type="text" v-model="mymessage" @change="changeValue" /> 
    </div>
  `,
  data() {
    return {
      mymessage: this.$parent.message
    }
  },
  methods: {
    changeValue(){
      this.$parent.message = this.mymessage;//通過如此呼叫可以改變父元件的值
    }
  },
});
    
const app = new Vue({
  el: '#app',
  template: `
    <div>
      <parent />
    </div>
  `
});
複製程式碼

在上面例項程式碼中,分別定義了 parent 和 child 元件,這兩個元件是直接的父子關係。兩個元件分別在內部定義了自己的屬性。在 parent 元件中,直接通過 this.$children[0].mymessage = 'hello';child 元件內的 mymessage 屬性賦值,而在 child 子元件中,同樣也是直接通過this.$parent.messageparent 元件中的 message 賦值,形成了父子元件通訊。

關於 $parent$children 這對屬性的詳細介紹可以查詢官網文件!

7、$boradcast$dispatch

這也是一對成對出現的方法,不過只是在 Vue1.0 中提供了,而 Vue2.0 被廢棄了,但是還是有很多開源軟體都自己封裝了這種元件通訊的方式,比如 Mint UIElement UIiView 等。

// broadcast 方法的主邏輯處理方法
function broadcast(componentName, eventName, params) {
  this.$children.forEach(child => {
    const name = child.$options.componentName;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else {
      broadcast.apply(child, [componentName, eventName].concat(params));
    }
  });
}

export default {
  methods: {
    // 定義 dispatch 方法
    dispatch(componentName, eventName, params) {
      let parent = this.$parent;
      let name = parent.$options.componentName;
      while (parent && (!name || name !== componentName)) {
        parent = parent.$parent;

        if (parent) {
          name = parent.$options.componentName;
        }
      }
      
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    
    // 定義 broadcast 方法
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params);
    }
  }
};
複製程式碼

上面所示的程式碼,一般都作為一個 mixins 去混入使用, broadcast 是向特定的父元件觸發事件,dispatch 是向特定的子元件觸發事件,本質上這種方式還是 onemit 的封裝,在一些基礎元件中都很實用。

因為在 Vue 2.0 這個 API 已經廢棄,那我們在這裡也就提一下,如果想詳細瞭解 Vue 1.0 和其他基於 Vue 的 UI 框架關於這個 API 的實現,可以點選檢視這篇文章

8、Vuex 狀態管理

Vuex 是狀態管理工具,實現了專案狀態的集中式管理。工具的實現借鑑了 FluxRedux、和 The Elm Architecture 的模式和概念。當然與其他模式不同的是,Vuex 是專門為 Vue.js 設計的狀態管理庫,以利用 Vue.js 的細粒度資料響應機制來進行高效的狀態更新。詳細的關於 Vuex 的介紹,你既可以去檢視官網文件,也可以檢視本專欄關於 Vuex 一系列的介紹。

總結

寫到這裡,Vue 中關於元件通訊的所有方式就介紹完了,是不是感覺還是頗豐的呢?其實還有另外的兩種方式可以實現元件的通訊,一是通過 Vue Router 通訊,二是通過瀏覽器本地儲存實現元件通訊。關於這兩種方式,這裡我就不講了,當然我會在本專欄中單獨開篇講解的,希望大家有興趣就去看看!

準確來說本文詳細講解了實現 Vue 通訊的六種方式,每種方式都有其特點。在實際的專案,大家可以酌情的進行使用。

相關文章