vue是資料驅動檢視更新的框架, 所以對於vue來說元件間的資料通訊非常重要;我們常用的方式莫過於通過props傳值給子元件,但是vue還有其他很多不常用的通訊方式,瞭解他們,也許在以後在寫程式碼的時候能給你帶來更多的思路和選擇。
prop/$emit
父元件通過prop
的方式向子元件傳遞資料,而通過$emit
子元件可以向父元件通訊。
//Parent.vue
<template>
<div>
當前選中:{{ current }}
<Child :list="list" @change="changeCurrent"></Child>
</div>
</template>
<script>
import Child from "./child";
export default {
data() {
return {
current: 0,
list: ["紅樓夢", "水滸傳", "三國演義", "西遊記"]
};
},
components: { Child },
methods: {
changeCurrent(num) {
this.current = num;
}
}
};
</script>
複製程式碼
我們可以通過prop
向子元件傳遞資料;用一個形象的比喻來說,父子元件之間的資料傳遞相當於自上而下的下水管子,管子中的水就像資料,水只能從上往下流,不能逆流。這也正是Vue的設計理念之單向資料流。而prop
正是管道與管道之間的一個銜介面,這樣水(資料)才能往下流。
//Child.vue
<template>
<div>
<template v-for="(item, index) in list">
<div @click="clickItem(index)" :key="index">{{ item }}</div>
</template>
</div>
</template>
<script>
export default {
props: {
list: {
type: Array,
default: () => {
return [];
}
}
},
methods: {
clickItem(index) {
this.$emit("change", index);
}
}
};
</script>
複製程式碼
在子元件中我們通過props物件定義了接收父元件值的型別和預設值,然後通過$emit()
觸發父元件中的自定義事件。prop/$emit
傳遞資料的方式在日常開發中用的非常多,一般涉及到元件開發都是基於通過這種方式;通過父元件中註冊子元件,並在子元件標籤上繫結對自定義事件的監聽。他的優點是傳值取值方便簡潔明瞭,但是這種方式的缺點是:
- 由於資料是單向傳遞,如果子元件需要改變父元件的props值每次需要給子元件繫結對應的監聽事件。
- 如果父元件需要給孫元件傳值,需要子元件進行轉發,較為不便。
.sync修飾符
有些情況下,我們希望在子元件能夠“直接修改”父元件的prop值,但是雙向繫結會帶來維護上的問題;vue提供了一種解決方案,通過語法糖.sync修飾符。
.sync修飾符在 vue1.x
的時候曾作為雙向繫結功能存在,即子元件可以修改父元件中的值。但是它違反了單向資料流的設計理念,所以在 vue2.0
的時候被幹掉了。但是在 vue2.3.0+
以上版本又重新引入了。但是這次它只是作為一個編譯時的語法糖存在。它會被擴充套件為一個自動更新父元件屬性的v-on
監聽器。說白了就是讓我們手動進行更新父元件中的值了,從而使資料改動來源更加的明顯。
//Parent.vue
<template>
<div>
<Child :msg.sync="msg" :num.sync="num"></Child>
</div>
</template>
<script>
import Child from "./child";
export default {
name: "way2",
components: {
Child
},
data() {
return {
msg: "hello every guys",
num: 0
};
}
};
</script>
複製程式碼
我們在Child元件傳值時給每個值新增一個.sync修飾,在編譯時會被擴充套件為如下程式碼:
<Child :msg="msg" @update.msg="val => msg = val" :num.sync="num" @update.num="val => num = val"></Child>
複製程式碼
因此子元件中只需要顯示的觸發update的更新事件:
//Child.vue
<template>
<div>
<div @click="clickRevert">點選更新字串:{{ msg }}</div>
<div>當前值:{{ num }}</div>
<div @click="clickOpt('add')" class="opt">+</div>
<div @click="clickOpt('sub')" class="opt">-</div>
</div>
</template>
<script>
export default {
props: {
msg: {
type: String,
default: ""
},
num: {
type: Number,
default: 0
}
},
methods: {
clickRevert() {
let { msg } = this;
this.$emit("update:msg",msg.split("").reverse().join(""));
},
clickOpt(type = "") {
let { num } = this;
if (type == "add") {
num++;
} else {
num--;
}
this.$emit("update:num", num);
}
}
};
</script>
複製程式碼
這種“雙向繫結”的操作是不是看著似曾相識?是的,v-model本質上也是一種語法糖,只不過它觸發的不是update方法而是input方法;而且v-model沒有.sync來的更加靈活,v-model只能繫結一個值。
總結:.sync修飾符優化了父子元件通訊的傳值方式,不需要在父元件再寫多餘的函式來修改賦值。
attrs和listeners
當需要用到從A到C的跨級通訊時,我們會發現prop傳值非常麻煩,會有很多冗餘繁瑣的轉發操作;如果C中的狀態改變還需要傳遞給A,使用事件還需要一級一級的向上傳遞,程式碼可讀性就更差了。
因此vue2.4+
版本提供了新的方案:$attrs和$listeners
,我們先來看一下官網對$attrs的描述:
包含了父作用域中不作為 prop 被識別 (且獲取) 的特性繫結 (class 和 style 除外)。當一個元件沒有宣告任何 prop 時,這裡會包含所有父作用域的繫結 (class 和 style 除外),並且可以通過 v-bind="$attrs" 傳入內部元件——在建立高階別的元件時非常有用。
這一大段話第一次讀非常的繞口,而且晦澀難懂,不過沒關係,我們直接上程式碼:
//Parent.vue
<template>
<div>
<Child
:notUse="'not-use'"
:childMsg="childMsg"
:grandChildMsg="grandChildMsg"
@onChildMsg="onChildMsg"
@onGrandChildMsg="onGrandChildMsg"
></Child>
</div>
</template>
<script>
import Child from "./child";
export default {
data() {
return {
childMsg: "hello child",
grandChildMsg: "hello grand child"
};
},
components: { Child },
methods: {
onChildMsg(msg) {
this.childMsg = msg;
},
onGrandChildMsg(msg) {
this.grandChildMsg = msg;
}
}
};
</script>
複製程式碼
我們首先定義了兩個msg,一個給子元件展示,另一個給孫元件展示,首先將這兩個資料傳遞到子元件中,同時將兩個改變msg的函式傳入。
//child.vue
<template>
<div class="box">
<div @click="clickMsg">{{ childMsg }}</div>
<div>$attrs: {{ $attrs }}</div>
<GrandChild v-bind="$attrs" v-on="$listeners"></GrandChild>
</div>
</template>
<script>
import GrandChild from "./grand-child";
export default {
props: {
childMsg: {
type: String
}
},
methods: {
clickMsg() {
let { childMsg } = this;
this.$emit(
"onChildMsg",
childMsg.split("").reverse().join("")
);
}
},
components: { GrandChild }
};
</script>
複製程式碼
在子元件中我們通過props獲取子元件所需要的引數,即childMsg;剩餘的引數就被歸到了$attrs
物件中,我們可以在頁面中展示出來,然後把它繼續往孫元件中傳;同時把所有的監聽函式歸到$listeners,也繼續往下傳。
//grand-child.vue
<template>
<div class="box1" @click="clickMsg">grand-child:{{ grandChildMsg }}</div>
</template>
<script>
export default {
props: {
grandChildMsg: {
type: String
}
},
methods: {
clickMsg() {
let { grandChildMsg } = this;
this.$emit(
"onGrandChildMsg",
grandChildMsg.split("").reverse().join("")
);
}
}
};
</script>
複製程式碼
在孫元件中我們繼續取出所需要的資料進行展示或者操作,執行結果如下:
當我們在元件上賦予一個非prop宣告時,比如child元件上的notuse和grandchildmsg屬性我們沒有用到,編譯之後的程式碼會把這個屬性當成原始屬性對待,新增到html原生標籤上,所以我們檢視程式碼是這樣的:
這樣會很難看,我們可以在元件上加上inheritAttrs
屬性將它去掉:
export default {
mounted(){},
inheritAttrs: false,
}
複製程式碼
總結:$attrs和$listeners
很好的解決了跨一級元件傳值的問題。
provide和inject
雖然$attrs和$listeners
可以很方便的從父元件傳值到孫元件,但是如果跨了三四級,並且想要的資料已經被上級元件取出來,這時$attrs就不能解決了。
provide/inject是vue2.2+
版本新增的屬性,簡單來說就是父元件中通過provide來提供變數, 然後再子元件中通過inject來注入變數。這裡inject注入的變數不像$attrs
,只能向下一層;inject不論子元件巢狀有多深,都能獲取到。
//Parent.vue
<template>
<div>
<Child></Child>
</div>
</template>
<script>
import Child from "./child";
export default {
components: { Child },
data() {
return {
childmsg: "hello child",
grandmsg: "hello grand child"
};
},
provide() {
return {
childmsg: this.childmsg,
grandmsg: this.grandmsg
};
},
mounted() {
setTimeout(() => {
this.childmsg = "hello new child";
this.grandmsg = "hello new grand child";
}, 2000);
},
};
</script>
複製程式碼
我們在父元件通過provide注入了兩個變數,並且在兩秒之後修改變數的值,然後就在子元件和孫元件取出來。
//child.vue
<template>
<div class="box">
<div>child-msg:{{ childmsg }}</div>
<div>grand-msg:{{ grandmsg }}</div>
<GrandChild></GrandChild>
</div>
</template>
<script>
import GrandChild from "./grand-child";
export default {
inject: ["childmsg", "grandmsg"],
components: { GrandChild },
};
</script>
//grand-child.vue
<template>
<div class="box">
<div>child-msg:{{ childmsg }}</div>
<div>grand-msg:{{ grandmsg }}</div>
</div>
</template>
<script>
export default {
name: "GrandChild",
inject: ["childmsg", "grandmsg"],
};
</script>
複製程式碼
可以看到子元件和孫元件都能取出值,並且渲染出來。需要注意的是,一旦子元件注入了某個資料,在data中就不能再宣告這個資料了。
同時,過了兩秒後我們發現childmsg和grandmsg的值並沒有按照預期的改變,也就是說子元件並沒有響應修改後的值,官網的介紹是這麼說的:
提示:
provide
和inject
繫結並不是可響應的。這是刻意為之的。然而,如果你傳入了一個可監聽的物件,那麼其物件的屬性還是可響應的。
vue並沒有把provide和inject設計成響應式的,這是vue故意的,但是如果傳入了一個可監聽的物件,那麼就可以響應了:
export default {
data() {
return {
respondData: {
name: "hello respond"
}
};
},
provide() {
return {
respondData: this.respondData
};
},
mounted() {
setTimeout(() => {
this.respondData.name = this.respondData.name
.split("")
.reverse()
.join("");
}, 2000);
},
}
複製程式碼
那麼為什麼上面的props和$attrs都是響應式的,連破壞“單向資料流”的.sync
修飾符都是響應式的,但到了provide/inject就不是響應式的了呢?在網上找了半天的資料也沒有找到確切的答案,本文就此結束。
就這麼結束了嗎?當然沒有!在一(zi)個(ji)哥(xue)們(xi)的幫(yuan)助(ma)下,我總算找到了答案。首先我們試想一下,如果有多個子元件同時依賴於一個父元件提供的資料,那麼一旦父元件修改了該值,那麼所有元件都會受到影響,這是我們不希望看到的;這一方面增加了耦合度,另一方面使得資料變化不可控制。接著看一下vue是怎麼來實現provide/inject的。
//src/core/instance/inject.js
//部分核心原始碼
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
observerState.shouldConvert = false
Object.keys(result).forEach(key => {
defineReactive(vm, key, result[key])
})
observerState.shouldConvert = true
}
}
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject).filter(key => {
return Object.getOwnPropertyDescriptor(inject, key).enumerable
})
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && provideKey in source._provided) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
}
}
}
return result
}
}
複製程式碼
可以看到初始化provide的時候將父元件的provide掛載到_provided
,但它不是一個響應式的物件;然後子元件通過$parent
向上查詢所有父元件的_provided
獲取第一個有目標屬性的值,然後遍歷繫結到子元件上;因為只是初始化的時候繫結的,而且_provided
也不是響應式的,所以造成了provide/inject的這種特性。
那麼provide/inject這麼危險,又不是響應式的,它能拿來做什麼呢?開啟element-ui
的原始碼搜尋provide,我們可以看到非常多的元件使用了provide/inject,我們就拿form、form-item和button舉個例子。
form和form-item都可以傳入一個屬性size來控制子元件的尺寸,但是子元件的位置是不固定的,可能會巢狀了好幾層el-row或者el-col,如果一層一層的通過props傳size下去會很繁瑣,這是provide/inject就派上用處了。
//form-item.vue
export default {
provide() {
return {
elFormItem: this
};
},
}
//button.vue
export default {
inject: {
elForm: {
default: ''
},
elFormItem: {
default: ''
}
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
buttonSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
},
}
複製程式碼
我們通過父元件將elFormItem本身注入到子元件中,子元件通過inject獲取父元件本身然後動態地計算buttonSize。
總結:provide/inject能夠解決多層元件巢狀傳值的問題,但是是非響應的,即provide與inject之間沒有繫結,注入的值是在子元件初始化過程中決定的。
EventBus
EventBus
我剛開始直接翻譯理解為事件車
,但比較官方的翻譯是事件匯流排
。它的實質就是建立一個vue例項,通過一個空的vue例項作為橋樑實現vue元件間的通訊。它是實現非父子元件通訊的一種解決方案,所有的元件都可以上下平行地通知其他元件,但也就是太方便所以若使用不慎,就會造成難以維護的“災難”。
//utils/event-bus.js
import Vue from "vue";
export default new Vue();
複製程式碼
首先創造一個空的vue物件並將其匯出,他是一個不具備DOM
的元件,它具有的僅僅只是它例項方法而已,因此它非常的輕便。
//main.js
import bus from "@/utils/event-bus";
Vue.prototype.$bus = bus;
複製程式碼
將其掛載到全域性,變成全域性的事件匯流排,這樣在元件中就能很方便的呼叫了。
//Parent.vue
<template>
<div class="box">
<Child1></Child1>
<Child2></Child2>
</div>
</template>
<script>
import Child1 from "./child1";
import Child2 from "./child2";
export default {
components: {
Child1,
Child2
}
};
</script>
複製程式碼
我們先定義了兩個子元件child1和child2,我們希望這兩個元件能夠直接給對方傳送訊息。
//child1.vue
<template>
<div>
<div class="send" @click="clickSend">傳送訊息</div>
<template v-for="(item, index) in msgList">
<div :key="index">{{ item }}</div>
</template>
</div>
</template>
<script>
export default {
data() {
return { msgList: [] };
},
mounted() {
this.$bus.$on("getMsg1", res => {
this.msgList.push(res);
});
},
methods: {
clickSend() {
this.$bus.$emit("getMsg2", "hello from1:" + parseInt(Math.random() * 20));
}
}
};
</script>
//child2.vue
<template>
<div>
<div class="send" @click="clickSend">傳送訊息</div>
<template v-for="(item, index) in msgList">
<div :key="index">{{ item }}</div>
</template>
</div>
</template>
<script>
export default {
data() {
return { msgList: [] };
},
mounted() {
this.$bus.$on("getMsg2", res => {
this.msgList.push(res);
});
},
methods: {
clickSend() {
this.$bus.$emit("getMsg1", "hello from2:" + parseInt(Math.random() * 20));
}
}
};
</script>
複製程式碼
我們初始化時在child1和child2中分別註冊了兩個接收事件,然後點選按鈕時分別觸發這兩個自定義的事件,並傳入資料,最後兩個元件分別能接收到對方傳送的訊息,最後效果如下:
前面也提到過,如果使用不善,EventBus會是一種災難,到底是什麼樣的“災難”了?大家都知道vue是單頁應用,如果你在某一個頁面重新整理了之後,與之相關的EventBus會被移除,這樣就導致業務走不下去。還要就是如果業務有反覆操作的頁面,EventBus在監聽的時候就會觸發很多次,也是一個非常大的隱患。這時候我們就需要好好處理EventBus在專案中的關係。通常會用到,在頁面或元件銷燬時,同時移除EventBus事件監聽。
export default{
destroyed(){
$EventBus.$off('event-name')
}
}
複製程式碼
總結:EventBus可以用來很方便的實現兄弟元件和跨級元件的通訊,但是使用不當時也會帶來很多問題;所以適合邏輯並不複雜的小頁面,邏輯複雜時還是建議使用vuex。
vuex
在vue元件開發中,經常會遇到需要將當前元件的狀態傳遞給其他非父子元件元件,或者一個狀態需要共享給多個元件,這時採用上面的方式就會非常麻煩。vue提供了另一個庫vuex來解決資料傳遞的問題;剛開始上手會感覺vuex非常的麻煩,很多概念也容易混淆,不過不用擔心,本文不深入講解vuex。
vuex實現了單向的資料流,在全域性定義了一個State物件用來儲存資料,當元件要修改State中的資料時,必須通過Mutation進行操作。
//store/count.js
export default {
namespaced: true,
state: { num: 1 },
mutations: {
ADD_NUM(state) {
state.num = state.num + 1;
},
SUB_NUM(state) {
state.num = state.num - 1;
}
},
actions: {
ADD_SYNC({ commit }) {
setTimeout(() => {
commit("ADD_NUM");
}, 1000);
},
SUB_SYNC({ commit }) {
setTimeout(() => {
commit("SUB_NUM");
}, 1000);
}
}
};
//store/index.js
import count from "./count";
export default new Vuex.Store({
modules: {
count
},
});
複製程式碼
我們首先在全域性定義了count.js
模組用來存放資料和修改資料的方法,然後在全域性引入。
//child.vue
<template>
<div>
<div>當前:{{ num }}</div>
<div class="opt" @click="clickAdd">+</div>
<div class="opt" @click="clickSub">-</div>
<div class="opt" @click="clickAddSync">a+</div>
<div class="opt" @click="clickSubSync">a-</div>
</div>
</template>
<script>
export default {
name: "Child",
computed: {
num() {
return this.$store.state.count.num;
}
},
methods: {
clickAdd() {
this.$store.commit("count/ADD_NUM");
},
clickSub() {
this.$store.commit("count/SUB_NUM");
},
clickAddSync() {
this.$store.dispatch("count/ADD_SYNC");
},
clickSubSync() {
this.$store.dispatch("count/SUB_SYNC");
}
}
};
</script>
複製程式碼
我們就可以在任何元件中來呼叫mutations和actions中的方法運算元據了。vuex在資料傳值和運算元據維護起來比較方便,但是有一定的學習成本。
$refs
有時候我們需要在vue中直接來操作DOM元素,比如獲取DIV的高度,或者直接呼叫子元件的一些函式;雖然原生的JS也能獲取到,但是vue為我們提供了更方便的一個屬性:$refs
。如果在普通的DOM元素上使用,獲取到的就是DOM元素;如果用在子元件上,獲取的就是元件的例項物件。
//child.vue
<template>
<div>初始化:{{ num }}</div>
</template>
<script>
export default {
data() {
return { num: 0 };
},
methods: {
addNum() {
this.num += 1;
},
subNum() {
this.num -= 1;
}
}
};
</script>
複製程式碼
我們首先建立一個簡單的子元件,有兩個函式用來增減num的值。
<template>
<div>
<Child ref="child"></Child>
<div class="opt" ref="opt_add" @click="clickAddBtn">+</div>
<div class="opt" ref="opt_sub" @click="clickSubBtn">-</div>
<div class="opt" ref="opt_show" @click="clickShowBtn">show</div>
</div>
</template>
<script>
import Child from "./child";
export default {
components: { Child },
data() {
return {};
},
methods: {
clickAddBtn() {
this.$refs.child.addNum();
},
clickSubBtn() {
this.$refs.child.subNum();
},
clickShowBtn() {
console.log(this.$refs.child);
console.log(this.$refs.child.num);
}
}
};
</script>
複製程式碼
我們給子元件增加一個ref屬性child,然後通過$refs.child
來獲取子元件的例項,通過例項來呼叫子元件中的函式。
可以看到我們獲取到的是一個VueComponent
物件,這個物件包括了子元件的所有資料和函式,可以對子元件進行一些操作。
parent和children
如果頁面有多個相同的子元件需要操作的話,$refs
一個一個操作起來比較繁瑣,vue提供了另外的屬性:$parent和$children
來統一選擇。
//child.vue
<template>
<div>child</div>
</template>
<script>
export default {
mounted() {
console.log(this.$parent.show());
console.log("Child", this.$children, this.$parent);
}
};
</script>
//Parent.vue
<template>
<div>
parent
<Child></Child>
<Child></Child>
</div>
</template>
<script>
import Child from "./child";
export default {
components: { Child },
mounted() {
console.log("Parent", this.$children, this.$parent);
},
methods: {
show() {
return "to child data";
}
}
};
</script>
複製程式碼
我們在父元件中插入了兩個相同的子元件,在子元件中通過$parent
呼叫了父元件的函式,並在父元件通過$children
獲取子元件例項的陣列。
我們在Parent中列印出$parent
屬性看到是最外層#app的例項。
因此我們把常見使用場景分為以下三類:
- 父子元件通訊:
props
;$parent/$children
;provide/inject
;$ref
;$attrs/$listeners
- 兄弟元件通訊:
EventBus
;Vuex
- 跨級通訊:
EventBus
;Vuex
;provide/inject
;$attrs/$listeners
本文的所有程式碼存放在github
更多前端資料請關注公眾號【前端壹讀】
。