vue 組件間的通訊是 vue 開發中很基礎也十分重要的部分,做爲使用 vue 的開發者天天都在使用。 同時,vue 通訊也是面試中很是高頻的問題,有不少面試題,都是圍繞通訊展開。javascript
本文會介紹常見的通訊方式,並分析每種方式的使用場景和注意點。html
vue中提倡單向數據流,這是爲了保證數據流向的簡潔性,使程序更易於理解。但對於一些邊界狀況,vue也提供了隱性的通訊方式,這些通訊方式會打破單向數據流的原則,應該謹慎使用。前端
下面咱們將組件通訊分爲父子組件通訊 和 非父子組件通訊進行分析。vue
prop
和 events
最基礎也最經常使用,這裏不提供示例。
經過 prop
向下傳遞,經過事件向上傳遞是一個 vue 項目最理想的通訊狀態。
使用時有兩點須要注意:java
第一,不該該在一個子組件內部改變
prop
,這樣會破壞單向的數據綁定,致使數據流難以理解。若是有這樣的須要,能夠經過data
屬性接收或使用computed
屬性進行轉換。
第二,若是props
傳遞的是引用類型(對象或者數組),在子組件中改變這個對象或數組,父組件的狀態會也會作相應的更新,利用這一點就可以實現父子組件數據的「雙向綁定」,雖然這樣實現可以節省代碼,但會犧牲數據流向的簡潔性,使人難以理解,最好不要這樣去作。想要實現父子組件的數據「雙向綁定」,可使用v-model
或.sync
。面試
v-model
是用來在表單控件或者組件上建立雙向綁定的,他的本質是 v-bind
和 v-on
的語法糖,在一個組件上使用 v-model
,默認會爲組件綁定名爲 value
的 prop
和名爲 input
的事件。
當咱們組件中的某一個 prop
須要實現上面所說的」雙向綁定「時,v-model
就能大顯身手了。有了它,就不須要本身手動在組件上綁定監聽當前實例上的自定義事件,會使代碼更簡潔。vuex
下面以一個 input 組件實現的核心代碼,介紹下 v-model
的應用。數組
<!--父組件-->
<template>
<base-input v-model="input"></base-input>
</template>
<script> export default { data() { return { input: '' } }, } </script>
複製代碼
<!--子組件-->
<template>
<input type="text" :value="currentValue" @input="handleInput">
</template>
<script> export default { data() { return { currentValue: this.value === undefined || this.value === null ? '' } }, props: { value: [String, Number], }, methods: { handleInput(event) { const value = event.target.value; this.$emit('input', value); }, }, } </script>
複製代碼
有時,在某些特定的控件中名爲 value
的屬性會有特殊的含義,這時能夠經過 model
選項來回避這種衝突。app
.sync
修飾符在 vue 1.x 的版本中就已經提供,1.x 版本中,當子組件改變了一個帶有 .sync
的 prop
的值時,會將這個值同步到父組件中的值。這樣使用起來十分方便,但問題也十分明顯,這樣破壞了單向數據流,當應用複雜時,debug 的成本會很是高。因而乎,在vue 2.0中移除了 .sync
。 可是在實際的應用中,.sync
是有它的應用場景的,因此在 vue 2.3 版本中,又迎來了全新的 .sync
。iview
新的 .sync
修飾符所實現的已經再也不是真正的雙向綁定,它的本質和 v-model
相似,只是一種縮寫。
<text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event" ></text-document>
複製代碼
上面的代碼,使用 .sync
就能夠寫成
<text-document v-bind:title.sync="doc.title"></text-document>
複製代碼
這樣,在子組件中,就能夠經過下面代碼來實現對這個 prop
從新賦值的意圖了。
this.$emit('update:title', newTitle)
複製代碼
v-model 和 .sync 對比
.sync
從功能上看和v-model
十分類似,都是爲了實現數據的「雙向綁定」,本質上,也都不是真正的雙向綁定,而是語法糖。
相比較之下,.sync
更加靈活,它能夠給多個prop
使用,而v-model
在一個組件中只能有一個。
從語義上來看,v-model
綁定的值是指這個組件的綁定值,好比 input 組件,select 組件,日期時間選擇組件,顏色選擇器組件,這些組件所綁定的值使用v-model
比較合適。其餘狀況,沒有這種語義,我的認爲使用.sync
更好。
ref 特性能夠爲子組件賦予一個 ID 引用,經過這個 ID 引用能夠直接訪問這個子組件的實例。 當父組件中須要主動獲取子組件中的數據或者方法時,可使用 $ref
來獲取。
<!--父組件-->
<template>
<base-input ref="baseInput"></base-input>
</template>
<script> export default { methods: { focusInput: function () { this.$refs.usernameInput.focus() } } } </script>
複製代碼
<!--子組件-->
<template>
<input ref="input">
</template>
<script> export default { methods: { focus: function () { this.$refs.input.focus() } } } </script>
複製代碼
使用 ref 時,有兩點須要注意
$refs
是做爲渲染結果被建立的,因此在初始渲染的時候它還不存在,此時沒法沒法訪問。$refs
不是響應式的,只能拿到獲取它的那一刻子組件實例的狀態,因此要避免在模板和計算屬性中使用它。
$parent
屬性能夠用來從一個子組件訪問父組件的實例,$children
屬性 能夠獲取當前實例的直接子組件。
看起來使用 $parent
比使用prop傳值更加簡單靈活,能夠隨時獲取父組件的數據或方法,又不像使用 prop
那樣須要提早定義好。但使用 $parent
會致使父組件數據變動後,很難去定位這個變動是從哪裏發起的,因此在絕大多數狀況下,不推薦使用。
在有些場景下,兩個組件之間多是父子關係,也多是更多層嵌套的祖孫關係,這時就可使用 $parent
。
下面是 element ui 中的組件 el-radio-group 和 組件 el-radio 使用示例:
<template>
<el-radio-group v-model="radio1">
<el-radio :label="3">備選項</el-radio>
<component-1>
<el-radio :label="3">備選項</el-radio>
</component-1>
</el-radio-group>
</template>
<script> export default { data () { return { radio2: 3 }; } } </script>
複製代碼
在 el-radio-group 和 組件 el-radio 通訊中, 組件 el-radio 的 value 值須要和 el-radio-group的 v-model
的值進行「綁定」,咱們就能夠在 el-radio 內藉助 $parent
來訪問到 el-radio-group 的實例,來獲取到 el-radio-group 中 v-model
綁定的值。
下面是獲取 el-radio 組件中獲取 el-radio-group 實例的源碼:
// el-radio組件
let parent = this.$parent;
while (parent) {
if (parent.$options.componentName !== 'ElRadioGroup') {
parent = parent.$parent;
} else {
this._radioGroup = parent; // this._radioGroup 爲組件 el-radio-group 的實例
}
}
複製代碼
當要和一個嵌套很深的組件進行通訊時,若是使用 prop
和 events
就會顯的十分繁瑣,中間的組件只起到了一箇中轉站的做用,像下面這樣:
<!--父組件-->
<parent-component :message="message">我是父組件</parent-component>
<!--子組件-->
<child-component :message="message">我是子組件</child-component>
<!--孫子組件-->
<grand-child-component :message="message">我是孫子組件</grand-child-component>
複製代碼
當要傳遞的數據不少時,就須要在中間的每一個組件都重複寫不少遍,反過來從後代組件向祖先組件使用 events 傳遞也會有一樣的問題。使用 $attrs
和 $listeners
就能夠簡化這樣的寫法。
$attrs
會包含父組件中沒有被 prop
接收的全部屬性(不包含class 和 style 屬性),能夠經過 v-bind="$attrs"
直接將這些屬性傳入內部組件。
$listeners
會包含全部父組件中的 v-on
事件監聽器 (不包含 .native
修飾器的) ,能夠經過 v-on="$listeners"
傳入內部組件。
下面以父組件和孫子組件的通訊爲例介紹它們的使用:
<!--父組件 parent.vue-->
<template>
<child :name="name" :message="message" @sayHello="sayHello"></child>
</template>
<script> export default { inheritAttrs: false, data() { return { name: '通訊', message: 'Hi', } }, methods: { sayHello(mes) { console.log('mes', mes) // => "hello" }, }, } </script>
複製代碼
<!--子組件 child.vue-->
<template>
<grandchild v-bind="$attrs" v-on="$listeners"></grandchild>
</template>
<script> export default { data() { return {} }, props: { name, }, } </script>
複製代碼
<!--孫子組件 grand-child.vue-->
<template>
</template>
<script> export default { created() { this.$emit('sayHello', 'hello') }, } </script>
複製代碼
provide
和 inject
須要在一塊兒使用,它可使一個祖先組件向其全部子孫後代注入一個依賴,能夠指定想要提供給後代組件的數據/方法,不論組件層次有多深,都可以使用。
<!--祖先組件-->
<script> export default { provide: { author: 'yushihu', }, data() {}, } </script>
複製代碼
<!--子孫組件-->
<script> export default { inject: ['author'], created() { console.log('author', this.author) // => yushihu }, } </script>
複製代碼
provide
和 inject
綁定不是響應的,它被設計是爲組件庫和高階組件服務的,日常業務中的代碼不建議使用。
vue 在2.0版本就已經移除了 $dispatch
和 $broadcast
,由於這種基於組件樹結構的事件流方式會在組件結構擴展的過程當中會變得愈來愈難維護。但在某些不使用 vuex 的狀況下,仍然有使用它們的場景。因此 element ui 和 iview 等開源組件庫中對 broadcast
和 dispatch
方法進行了重寫,並經過 mixin 的方式植入到每一個組件中。
實現 dispatch
和 broadcast
主要利用咱們上面已經說過的 $parent
和 $children
。
//element ui 中重寫 broadcast 的源碼
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
複製代碼
broadcast
方法的做用是向後代組件傳值,它會遍歷全部的後代組件,若是後代組件的 componentName
與當前的組件名一致,則觸發 $emit
事件,將數據 params
傳給它。
//element ui 中重寫 dispatch 的源碼
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var 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));
}
},
複製代碼
dispatch
的做用是向祖先組件傳值,它會一直尋找父組件,直到找到組件名和當前傳入的組件名一致的祖先組件,就會觸發其身上的 $emit
事件,將數據傳給它。這個尋找對應的父組件的過程和文章前面講解 $parent
的例子相似。
對於比較小型的項目,沒有必要引入 vuex 的狀況下,可使用 eventBus
。相比咱們上面說的全部通訊方式,eventBus
能夠實現任意兩個組件間的通訊。
它的實現思想也很好理解,在要相互通訊的兩個組件中,都引入同一個新的vue實例,而後在兩個組件中經過分別調用這個實例的事件觸發和監聽來實現通訊。
//eventBus.js
import Vue from 'vue';
export default new Vue();
複製代碼
<!--組件A-->
<script> import Bus from 'eventBus.js'; export default { methods: { sayHello() { Bus.$emit('sayHello', 'hello'); } } } </script>
複製代碼
<!--組件B-->
<script> import Bus from 'eventBus.js'; export default { created() { Bus.$on('sayHello', target => { console.log(target); // => 'hello' }); } } </script>
複製代碼
經過 $root
,任何組件均可以獲取當前組件樹的根 Vue 實例,經過維護根實例上的 data
,就能夠實現組件間的數據共享。
//main.js 根實例
new Vue({
el: '#app',
store,
router,
// 根實例的 data 屬性,維護通用的數據
data: function () {
return {
author: ''
}
},
components: { App },
template: '<App/>',
});
複製代碼
<!--組件A-->
<script> export default { created() { this.$root.author = '因而乎' } } </script>
複製代碼
<!--組件B-->
<template>
<div><span>本文做者</span>{{ $root.author }}</div>
</template>
複製代碼
經過這種方式,雖然能夠實現通訊,但在應用的任何部分,任什麼時候間發生的任何數據變化,都不會留下變動的記錄,這對於稍複雜的應用來講,調試是致命的,不建議在實際應用中使用。
Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。對一箇中大型單頁應用來講是不二之選。
使用 Vuex 並不表明就要把全部的狀態放入 Vuex 管理,這樣作會讓代碼變的冗長,沒法直觀的看出要作什麼。對於嚴格屬於組件私有的狀態仍是應該在組件內部管理更好。
對於小型的項目,通訊十分簡單,這時使用 Vuex 反而會顯得冗餘和繁瑣,這種狀況最好不要使用 Vuex,能夠本身在項目中實現簡單的 Store。
//store.js
var store = {
debug: true,
state: {
author: 'yushihu!'
},
setAuthorAction (newValue) {
if (this.debug) console.log('setAuthorAction triggered with', newValue)
this.state.author = newValue
},
deleteAuthorAction () {
if (this.debug) console.log('deleteAuthorAction triggered')
this.state.author = ''
}
}
複製代碼
和 Vuex 同樣,store 中 state
的改變都由 store 內部的 action
來觸發,而且可以經過 log
保留觸發的痕跡。這種方式十分適合在不須要使用 Vuex 的小項目中應用。
與 $root
訪問根實例的方法相比,這種集中式狀態管理的方式可以在調試過程當中,經過 log
記錄來肯定當前變化是如何觸發的,更容易定位問題。
歡迎關注個人公衆號「前端小苑」,我會按期在上面更新原創文章。