Vue.js 在現今使用有多普遍不用多說,而 Vue 的一大特色就是組件化。本期要講的,即是 Vue 組件間通訊方式的總結,這也幾乎是近年 Vue 面試中的必考題。注:文中示例都基於 Vue 腳手架講解,會用到一些 Element UI 示例。javascript
【前端進階之路】會做爲一個新系列連載,後續會更多優質前端內容,感興趣的同窗不妨關注一下。 文章最後有 交流羣 和 公衆號,能夠一塊兒學習交流,感謝🍻。html
組件是能夠複用的 Vue 實例。 — Vue 官方文檔;前端
在進入主題以前,仍是決定先簡單聊聊組件。在 Vue 中,根據註冊方式的不一樣,能夠分爲:vue
顧名思義,全局註冊的組件,能夠用在 Vue 實例的任意模板中。可是帶來的隱患是,在 webpack 模塊化構建時,即使你沒有在項目中使用這個組件,依然會打包到最終的項目代碼中。而局部組件,則須要在使用到的實例中註冊該組件。java
// 全局註冊
// install.js
import Icon from './Icon.vue';
const install = {
install:function(Vue){
Vue.component('VIcon', Icon);
}
};
export default install;
// main.js
import install from './install.js'; // 引入全局插件
Vue.use(install); // 註冊
// 局部註冊
import VIcon from './Icon.vue';
export default{
components: {
VIcon
}
}
// 使用
<v-icon> </v-icon> 複製代碼
根據應用場景的不一樣,又能夠分爲:node
以上就是組件的簡單介紹,那咱們到底爲何要推崇組件化?組件化有什麼好處?複用?我我的認爲組件化最大的好處,即是解耦,易於項目管理。因此在大型項目管理中,組件化是很是有必要的。固然,這並非今天學習的重點,之後有機會再聊。webpack
正由於在 Vue 中到處都是組件,而咱們也偏向於組件化、模塊化。那咱們在一堆組件中,便須要解決一個問題 — 組件間通訊。下面,咱們就進入今天的主題,Vue 的組件間通訊。git
組件間通訊是咱們在 Vue 項目中不可避免的問題,深入瞭解了 Vue 組件間通訊的幾種方式,才能讓咱們在處理各類交互問題時遊刃有餘。github
Vue 中,最基本的通訊方式就是 Props,它是父子組件通訊中父組件傳值給子組件的一種方式。它容許以數組形式接收,可是更推薦你開啓類型檢查的形式。更詳細的類型檢查前往 vue 文檔。web
// communication.vue
<communication-sub v-bind="dataProps"></communication-sub>
// v-bind="dataProps" 等同於 :title="title",適用於多個參數一塊兒傳遞
···
data() {
return {
dataProps: {
title: '我是父組件的值',
}
}
}
// communication-sub.vue
<div class="communication-sub">
{{title}}
</div>
···
props: ['title']
// 更推薦開啓類型檢查
props: {
title: {
type: String,
required: true,
default: '' // 容許指定默認值,引用類型須要函數返回
}
}
···
複製代碼
咱們都知道,Props 是單向數據流,這是 Vue 爲了不子組件意外改變父組件的狀態,從而致使數據流向難以理解而作出的限制。因此 Vue 推薦須要改動的時候,經過改變父組件的值從而觸發 Props 的響應。或者,咱們能夠在接收非引用類型的值時,使用子組件自身的 data 作一次接收。
props: ['title'],
data: function () {
return {
text: this.title
};
}
複製代碼
爲何是非引用類型呢,由於在 JavaScript 中,引用類型的賦值,實際是內存地址的傳遞。因此上面栗子中的簡單賦值,顯然會指向同一個內存地址,因此若是是數組或是對象,你可能須要一次深拷貝。
let obj = JSON.parse(JSON.stringify(obj));
複製代碼
上面這個操做有一些缺陷,不能序列化函數、undefined、循環引用等,詳見傳送門,可是也能應付一些平常狀況了。
事實上,在 Props 是引用類型時,單獨修改對象、數組的某個屬性或下標,Vue 並不會拋出錯誤。固然,前提是你要很是清楚本身在作什麼,並寫好註釋,防止你的小夥伴們疑惑。
有的同窗可能知道,在組件上綁定的屬性,若是沒有在組件內部用 Props 聲明,會默認綁定到組件的根元素上去。仍是以前的栗子:
<communication-sub v-bind="dataProps" class="one" type="div"></communication-sub> 複製代碼
結果以下:
這是 Vue 默認處理的,並且,除了 class 和 style 採用合併策略,其它特性(如上慄 type)會替換掉原來根元素上的屬性值。固然,咱們也能夠顯示的在組件內部關閉掉這個特性:
...
inheritAttrs: false,
props: ['title']
複製代碼
利用 inheritAttrs,咱們還能夠方便的把組件綁定的其它特性,轉移到咱們指定的元素上。這就須要用到下一個咱們要講的 $attrs
了。
咱們在使用組件庫的時候常常會這麼寫:
<el-input v-model="input" placeholder="請輸入內容"></el-input> 複製代碼
實際渲染後:
能夠看到咱們指定的的 placeholder 是渲染在 input 上的,可是 input 並非根元素。難道都用 Props 聲明後,再賦值給 input?這種狀況就能夠用到 $attrs
了,改造一下咱們以前那個栗子。
// communication.vue
<template>
<div class="communication">
<communication-sub v-bind="dataProps" class="input" type="text" placeholder="請輸入內容">
</communication-sub>
</div>
</template>
<script> import communicationSub from './communication-sub.vue'; export default{ name: 'communication', data() { return { dataProps: { title: '我是 communication 的值', } } }, components: { communicationSub } } </script>
// communication-sub.vue
···
<div class="communication-sub">
<input v-bind="$attrs" v-model="title"></input>
</div>
···
export default {
inheritAttrs: false
}
複製代碼
能夠看到,type 已經轉移到了子元素 input 標籤上,可是 class 沒有。這是由於 inheritAttrs: false
選項不會影響 style 和 class 的綁定。能夠看出 $attrs
則是將沒有被組件內部 Props 聲明的傳值(也叫非 Props 特性)收集起來的一個對象,再經過 v-bind 將其綁定在指定元素上。這也是 Element 等組件庫採用的策略。
這裏須要注意一點,經過 $attrs 指定給元素的屬性,不會與該元素原有屬性發生合併或替換,而是以原有屬性爲準。舉個例子,假如我將上述 input 的 type 默認設置爲 password。
<input v-bind="$attrs" v-model="title" type="password"></input>
複製代碼
則不會採用 $attrs 中的 type: 'text',將以 password 爲準,因此若是須要默認值的屬性,建議不要用這種方式。
$listeners
同 $attrs
相似,能夠看作是一個包含了組件上全部事件監聽器(包括自定義事件、不包括.native修飾的事件)的對象。它也支持上述的寫法,適用於將事件安放於組件內指定元素上。
// communication.vue
<communication-sub v-bind="dataProps"
class="input"
type="text"
placeholder="請輸入內容"
@focus="onFocus" >
</communication-sub> ··· methods: { onFocus() { console.log('onFocus'); } } // communication-sub.vue <input v-bind="$attrs" v-model="title" v-on="$listeners"></input> 複製代碼
給以前的栗子綁定一個聚焦事件,在子組件中經過 $listeners
綁定給 input,則會在 input 聚焦時觸發。
那麼除了用在這種給組件內指定元素綁定特性和事件的狀況,還有哪些場景能夠用到呢?官方說明:在建立更高層次的組件時很是有用。好比在祖孫組件中傳遞數據,在孫子組件中觸發事件後要在祖輩中作相應更新。咱們繼續以前的栗子:在孫輩組件觸發點擊事件,而後在祖輩中修改相應的 data。
// communication.vue
<communication-sub v-bind="dataProps" @click="onCommunicationClick">
</communication-sub>
···
methods: {
onCommunicationClick() {
this.dataProps.title = '我是點擊以後的值';
}
};
// communication-sub.vue
<communication-min-sub v-on="$listeners"></communication-min-sub> // 子組件中將事件透傳到孫輩
// communication-min-sub.vue
<template>
<div class="communication-min-sub">
<p>我是 communication-min-sub</p>
<button v-on="$listeners">click</button>
</div>
</template>
<script> export default{ name: 'communication-min-sub', inheritAttrs: false } </script>
複製代碼
這樣就能很方便的在多級組件的子級組件中,快速訪問到父組件的數據和方法。正如在剛纔的例子中,button 點擊時,是直接調用的 communication.vue 中定義的方法。
上面的方法,在大多數多級組件嵌套的場景頗有用,但有時咱們遇到的並不必定是有父子關係的組件。好比基礎組件中的 Select 下拉選擇器。
<el-select v-model="value" placeholder="請選擇">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> 複製代碼
相信你們都使用過上慄或者相似於上慄的基礎組件,它們藉助 vue 插槽 實現。因此這個時候,el-select 和 el-option 之間的數據通訊,咱們以前的 $attrs
、$listeners
就沒有用武之地了。有同窗可能不太理解上面的代碼爲何要通訊,我簡單介紹一下 Element 的處理方式:
咱們能夠簡單的認爲(Element 源碼比這個要稍複雜,爲了方便理解,簡化一下,若有須要,可直接前往源碼閱讀),在 el-select 中有一個 input 元素,el-option 中是一列渲染好的 li。根據需求,咱們在選中某個 li 的時候,要通知 input 展現相應的數據。並且咱們在實際使用的時候,通常還伴隨 el-form、el-form-item等組件,因此迫切須要一種方式:
能夠容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。--- Vue 文檔
有同窗可能會想到,這種多級的能夠用 Vuex、EventBus等方式,固然能夠。只不過咱們如今的前提是基礎組件,通常第三方組件庫是不會增長一些額外的依賴的。事實上 Vue 自己並不推薦直接在業務中使用 provide、inject,通常在組件、插件庫用到的比較多。
可是在項目比較小、業務邏輯比較簡單的時候,咱們徹底沒必要特地引入 Vuex。只要使用得當,provide、inject 確實不失爲一種好辦法。說了這麼多,咱們來看一下具體用法,咱們將以前的栗子,改成用 provide、inject 來實現。
// communication.vue
<communication-sub v-bind="dataProps" >
</communication-sub>
// @click="onCommunicationClick" 移除以前綁定的時間
···
// 在 provide 添加子代須要接收的方法 onCommunicationClick,
// 也能夠直接指定爲 this,子代便能訪問父代全部的數據和方法。
provide: function () {
return {
onCommunicationClick: this.onCommunicationClick
}
},
methods: {
onCommunicationClick() {
this.dataProps.title = '我是點擊以後的值';
}
};
// communication-sub.vue
<communication-min-sub></communication-min-sub>
// 移除以前的 v-on="$listeners",由於在這個組件中不須要用到父組件的方法,因此不用作其它處理
// communication-min-sub.vue
<template>
<div class="communication-min-sub">
···
<button @click="onCommunicationClick">click</button>
// 移除 v-on="$listeners",而後綁定 inject 接收到的方法
</div>
</template>
<script> export default{ name: 'communication-min-sub', inject: ['onCommunicationClick'] // inject 接收父組件的方法 } </script>
複製代碼
這種寫法和以前的 $listeners 獲得的效果是同樣的,就再也不放圖了。你們能夠本身嘗試一下,也能夠前往源碼 webrtc-stream。
思考:有些同窗可能會想到,若是我在根實例,app.vue 中如此設置:
<script> export default { provide () { return { app: this // 設置app爲this } }, data () { return { userInfo: null, otherState: null } } } </script>
複製代碼
那這樣把全部的狀態管理都放在 app.data 中,全部的子代中不就能夠共享了嗎?是否是就不須要 Vuex 了呢?實際上,Vue 自己就提供了一個方法來訪問根實例 $root
,因此即便沒有 provide 也是能夠作到的。那爲何不這麼用呢?仍是前面提到的緣由,不利於追蹤維護,也失去了所謂狀態管理的意義。不過,若是你的項目足夠小的話,依然能夠這麼使用。
咱們前面一直說的都是子組件如何觸達父組件,那麼父組件能不能訪問到子組件呢?固然是能夠的。
簡單來講就是獲取元素的 Dom 對象和子組件實例。若是在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;若是用在子組件上,引用就指向組件實例。獲取 Dom 元素就是爲了進行一些 Dom 操做,須要注意的一點就是,要在 Dom 加載完成後使用,不然可能獲取不到。好比我要將以前 input 的字體顏色改爲紅色:
<input type="text" v-model="dataProps.title" ref="input">
...
mounted() {
this.$nextTick(_ => { // 確保 Dom 更新完成
this.$refs['input'].style.color = 'red';
});
}
// 這裏只是舉一個栗子,實際項目中的需求,最好經過 class 的方式,儘可能減小 Dom 操做。
複製代碼
那什麼狀況下須要獲取組件實例呢?好比父元素的某個狀態改變,須要子組件進行 http 請求更新數據。一般狀況下,咱們會選擇經過 Props 將狀態傳遞給子組件,而後子組件進行 Watch 監測,若是有變動,則進行相應操做。這個時候,咱們即可以選擇使用 ref。
<child ref="child"></child>
···
<script> export default { methods () { onStateChange() { // 變動狀態後直接調用子組件方法進行更新 this.$refs['child'].updateData(); } } } </script>
複製代碼
$children
、$parent
無獨有偶,$children
一樣能夠完成上面的任務。$children
和 $parent
,顧名思義,一個會找到當前組件的子組件,一個會找到當前組件的父組件。若是有多個子組件,須要依賴組件實例的 name 屬性。改寫一下上面的方法:
<script>
export default {
methods () {
onStateChange() { // 子組件返回的是一個數組,多個子組件用 $options.name 區分。
this.$children[0].updateData();
}
}
}
</script>
複製代碼
$parent
和 $children
用法同樣,不過 $parent
返回的父組件實例,不是數組,由於父組件確定只有一個。ref、parent、children 它們幾個的一個缺點就是沒法處理跨級組件和兄弟組件,後續咱們會介紹 dispatch 和 broadcast 方法,實現跨級通訊。
$emit
,想必你們都很是熟悉,咱們一般用做父子組件間通訊,咱們也叫它自定義事件。$emit
和 $on
都是組件自身的方法,$on
能夠監聽 $emit
派發的事件,$off
則用來取消事件監聽。這也是咱們下一個要講的通訊方式 EventBus 所依賴的原理。
// 父組件
<template>
<button-component @clickButton="clickButton"></button-component>
// 在父組件利用 v-on 監聽
</template>
<script> export default { methods: { clickButton () { ··· } } } </script>
// 子組件
<template>
<button @click="handleClick"></button>
</template>
<script> export default { methods: { handleClick () { // 觸發 $emit this.$emit('clickButton'); } }, mounted() { this.$on('clickButton', (...arr) => { // 也能夠本身監聽 $emit,雖然沒什麼用··· console.log(...arr); }) } } </script>
複製代碼
$emit
的痛點依然是不支持跨級和兄弟組件,Vue 官方推薦咱們使用一個新的 Vue 實例來作一個全局的事件通訊(或者叫中央事件總線···),也就是咱們要講的 EventBus。瞭解過的同窗都知道,正常的 bus,咱們通常會掛載到 Vue 的 prototype 上,方便全局調用。
// main.js
Vue.prototype.$bus = new Vue();
複製代碼
依舊改寫以前的栗子:
<!--communication.vue-->
<communication-sub v-bind="dataProps" >
</communication-sub>
···
beforeDestroy() { <!-- 實例銷燬時,須要卸載監聽事件 -->
this.$bus.$off('busClick');
},
created() { <!-- 監聽子組件觸發的 Bus 事件-->
this.$bus.$on('busClick', (data) => {
this.dataProps.title = data;
});
}
<!--communication-min-sub.vue-->
<template>
<div class="communication-min-sub">
<button @click="busClick">click bus</button>
<!--子組件觸發點擊事件-->
</div>
</template>
<script> export default{ methods: { busClick() { this.$bus.$emit('busClick', 'bus 觸發了'); } } } </script>
複製代碼
這是一個基礎的 EventBus 的實現。如今咱們設想一下,相似於 userInfo 這樣的信息,在不少頁面都須要用到,那咱們須要在許多頁面都作 $on
監聽的操做。那可否將這些操做整合到一塊兒呢?咱們一塊兒來看:
// 新建一個 eventBus.js
import Vue from 'vue';
const bus = new Vue({
data () {
return {
userInfo: {}
}
},
created () {
this.$on('getUserInfo', val => {
this.userInfo = val;
})
}
});
export default bus;
// main.js
import bus from './eventBus';
Vue.prototype.$bus = bus;
// app.vue
methods: {
getUserInfo() {
ajax.post(***).then(data => {
this.$bus.$emit('getUserInfo', data); // 通知 EventBus 更新 userInfo
})
}
}
複製代碼
這樣在其餘頁面用到 userInfo 的時候,只須要 this.$bus.userInfo
就能夠了。注意剛剛其實沒有用 off 卸載掉監聽,由於其實 userInfo 這種全局信息,並無一個準確的說要銷燬的時機,瀏覽器關閉的時候,也用不着咱們處理了。可是,若是隻是某個頁面組件用到的,建議仍是用最開始的方法,在頁面銷燬的時候卸載掉。
不過反過來說,既然用到了 EventBus,說明狀態管理並不複雜,不然仍是建議用 Vuex 來作。最後再給你們推薦一篇文章 Vue中eventbus很頭疼?我來幫你,做者處理 EventBus 的思路很巧妙,你們不妨仔細看看。
此部分參考自 Element 源碼
在 Vue 1.x 的實現中,有 $dispatch
和 $broadcast
方法,可是在 2.x 被廢棄了。$dispatch
的主要做用是向上級組件派發事件,$broadcast
則是向下級廣播。它們的優勢是都支持跨級,再看一下官方廢棄這兩個方法的理由:
由於基於組件樹結構的事件流方式實在是讓人難以理解,而且在組件結構擴展的過程當中會變得愈來愈脆弱。而且
$dispatch
和$broadcast
也沒有解決兄弟組件間的通訊問題。
能夠看到,主要緣由是在組件結構擴展後不易理解,以及沒有解決兄弟組件通訊的問題。可是對於組件庫來講,這依舊是十分有用的,因此它們大多本身實現了這兩個方法。對咱們來說,也許在項目中用不到,但學習這種解決問題的思路,是十分必要的。
派發和廣播,依賴於組件的 name(最怕此處有人說:若是不寫 name,這方法不就沒用了?2333···),以此來逐級查找對應的組件實例。Element 的實現中,給全部的組件都加了一個 componentName 屬性,因此它是根據 componentName 來查找的。咱們在實現的時候仍是直接用 name。
咱們先來看一下 $dispatch
的簡單用法,再來分析思路。
<!--communication-min-sub.vue-->
<template>
<button @click="handleDispatch">dispatch</button>
</template>
<script> import Emitter from '../../utils/emitter'; export default { mixins: [Emitter], // 混入,方便直接調用 methods: { handleDispatch () { this.dispatch('communication', 'onMessage', '觸發了dispatch'); } } } </script>
複製代碼
<!--communication.vue-->
<script> export default { beforeDestroy() { // 銷燬 this.$off('onMessage'); }, mounted () { this.$on('onMessage', (data) => { // 監聽 this.dataProps.title = data; }) } } </script>
複製代碼
如今明確一下目標,dispatch 方法接收三個參數,組件 name、事件名稱、基礎數據(可不傳)。要作到向上跨級派發事件,須要向上找到指定 name 的組件實例,利用咱們前文提到的 $emit
方法作派送,因此在指定組件就能夠用 $on
來監聽了。因此 dispatch 本質上就是向上查找到指定組件並觸發其自身的 $emit
,以此來作響應,broadcast 則相反。那麼如何作到跨級查找呢?
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => { // 遍歷全部的 $children
var name = child.$options.name; // 拿到實例的name,Element 此處用的 componentName
if (name === componentName) { // 若是是想要的那個,進行廣播
child.$emit.apply(child, [eventName].concat(params));
} else { // 不是則遞歸查找 直到 $children 爲 []
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
// 存在 parent 且 (不存在 name 或 name 和 指定參數不同) 則繼續查找
parent = parent.$parent; // 不存在繼續取上級
if (parent) {
name = parent.$options.name; // 存在上級 再次賦值並再次循環,進行判斷
}
}
if (parent) { // 找到之後 若是有 進行事件派發
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
複製代碼
以上是詳細的 emitter.js,能夠看見,這和咱們以前講到的 $parent
、$children
、$emit
、$on
都密切相關。這也是爲何把它放到後面講的緣由。以前說過,派發和廣播並無解決兄弟組件通訊的問題,因此這裏你們也能夠拓展思考一下,如何支持兄弟組件間通訊。依然是依賴於$parent
、$children
,能夠找到任意指定組件。
Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。--- 官方文檔
Vuex 相信你們都比較熟悉了,我不打算在這裏把 API 再演示一遍。由於我以爲,官方文檔 已經很是詳細了。Vuex 的核心是單向數據流,並以相應規則保證全部的狀態管理均可追蹤、可預測。
咱們須要知道何時該用 Vuex,若是你的項目比較小,狀態管理比較簡單,徹底沒有必要使用 Vuex,你能夠考慮咱們前文提到的幾種方式。
本期文章內容到這裏就講完了,咱們來總結回顧一下:
$parent
、$attrs
、$listeners
、provide 和 inject、$dispatch
$emit
和 $on
、$children
、$ref
、broadcast
原本想按照是否支持跨級來分,可是這裏的界定比較模糊:若是逐級傳遞,有些也能作到跨級,但這並非咱們想要的。因此咱們只要本身清楚在什麼狀況下該怎麼用就行了。
qq前端交流羣:960807765,歡迎各類技術交流,期待你的加入
若是你看到了這裏,且本文對你有一點幫助的話,但願你能夠動動小手支持一下做者,感謝🍻。文中若有不對之處,也歡迎你們指出,共勉。
更多文章:
歡迎關注公衆號 前端發動機,第一時間得到做者文章推送,還有各種前端優質文章,但願在將來的前端路上,與你一同成長。