眾所周知,Vue 主要思想之一就是元件式開發。因此,在實際的專案開發中,肯定會以元件的開發模式進行。形如頁面和頁面之間需要通訊一樣,Vue 元件和元件之間肯定也需要互通有無、共享狀態。接下來,我們就悉數給大家展示所有 Vue 元件之間的通訊方式。
元件關係
上面展示的圖片可以引入所有 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 所識別 (且獲取) 的特性繫結 (class
和style
除外)。當一個元件沒有宣告任何 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、provide
和 inject
熟悉 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 主要為高階外掛/元件庫提供用例,並不推薦直接用於應用程式程式碼中。
關於 provide
和 inject
這對屬性的更多具體用法可以參照官網的文件。
寫到這裡有點累了,前面大致介紹了四種 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.message
給 parent
元件中的 message
賦值,形成了父子元件通訊。
關於 $parent
和 $children
這對屬性的詳細介紹可以查詢官網文件!
7、$boradcast
和 $dispatch
這也是一對成對出現的方法,不過只是在 Vue1.0
中提供了,而 Vue2.0
被廢棄了,但是還是有很多開源軟體都自己封裝了這種元件通訊的方式,比如 Mint UI、Element UI 和 iView 等。
// 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
是向特定的子元件觸發事件,本質上這種方式還是 on
和 emit
的封裝,在一些基礎元件中都很實用。
因為在 Vue 2.0
這個 API 已經廢棄,那我們在這裡也就提一下,如果想詳細瞭解 Vue 1.0
和其他基於 Vue 的 UI 框架關於這個 API 的實現,可以點選檢視這篇文章!
8、Vuex 狀態管理
Vuex 是狀態管理工具,實現了專案狀態的集中式管理。工具的實現借鑑了 Flux、Redux、和 The Elm Architecture 的模式和概念。當然與其他模式不同的是,Vuex 是專門為 Vue.js 設計的狀態管理庫,以利用 Vue.js 的細粒度資料響應機制來進行高效的狀態更新。詳細的關於 Vuex 的介紹,你既可以去檢視官網文件,也可以檢視本專欄關於 Vuex 一系列的介紹。
總結
寫到這裡,Vue 中關於元件通訊的所有方式就介紹完了,是不是感覺還是頗豐的呢?其實還有另外的兩種方式可以實現元件的通訊,一是通過 Vue Router 通訊,二是通過瀏覽器本地儲存實現元件通訊。關於這兩種方式,這裡我就不講了,當然我會在本專欄中單獨開篇講解的,希望大家有興趣就去看看!
準確來說本文詳細講解了實現 Vue 通訊的六種方式,每種方式都有其特點。在實際的專案,大家可以酌情的進行使用。