一、bus模式簡介
1. bus簡例
bus是一種通過事件實現組件交互的通信模式,它藉助一個額外的Vue實例作爲事件管理中心。任何引入了該Vue實例的組件都處於同一個事件環路內,可以相互註冊和觸發事件。一個簡單的bus實例如下:
bus.js
import Vue from 'vue';
const bus = new Vue();
export default bus;
component1.vue
...
import bus from './bus.js';
bus.$on('move', (payload) => {
...
})
component2.vue
import bus from './bus.js';
...
bus.$emit('move', payload);
這裏bus是一個空Vue實例,我們在component1和component2中均引入了這個實例,並在component1中註冊了對move
事件的監聽。接下來我們在component2的合適時機下觸發move
事件,這樣component1就可以接收到這個事件,並觸發對應的回調。
如果bus.$on
註冊在組件內部(如methods或各個生命週期函數內),由於箭頭函數內部沒有this,因此你可以在回調函數內直接通過this訪問當前組件實例(如果回調函數是常規函數,可以將組件實例保存在一個局部變量內)。
那麼爲什麼需要這樣一個模式呢?
2. 爲什麼需要bus?
這是因爲,通常來說,在不借助bus模式的情況下,事件的觸發只會發生在父子組件之間,並且只能由子組件向父組件觸發事件。它的大致實現模式如下:
parent.vue
<template>
<child
@tick="handleTick"
></child>
</template>
<script>
export default {
methods: {
handleTick () { ... }
}
}
</script>
child.vue
...
this.$emit('tick');
...
父組件向子組件綁定了一個對tick
事件的監聽,並定義了處理函數,而子函數可以在恰當的時機向父組件觸發該事件。這種模式在封裝第三方組件時極其常用。
另一種不太常見的交互是,由父組件直接調用子組件內的方法:
parent.vue
<template>
<child ref="child"></child>
</template>
<script>
export default {
mounted () {
this.$refs.child.tick();
}
}
</script>
這裏父組件通過ref
屬性拿到了對子組件的引用,然後直接調用了子組件內定義的tick
方法。當然,真正的函數調用仍然發生在子組件內。這種交互不涉及事件。
上述兩種方法都只能作用於父子組件之間,對於無直接父子關係的組件則束手無策,這也是bus模式的使用背景。
二、bus的應用
1. bus原理
上文的例子中只展示了bus模式的簡單用法,在介紹其他用法之前,我們先通過一張圖來理解何爲bus模式:
由於bus是一個完整的Vue實例,因此它具備事件管理能力。我們在任意組件內導入bus,並通過bus.$on
註冊一個事件監聽時,該回調函數就會進入bus內的事件隊列。任何引入了bus的組件,都可以訂閱任何感興趣的事件(即註冊回調事件)。
事件的觸發是同樣的道理,我們只需要在組件內引入bus,然後通過bus.$emit
觸發一個事件即可。觸發了一個事件後,事件隊列中的所有回調函數都會按照註冊順序依次執行。
需要注意的是,回調函數的註冊和執行其實都是發生在bus實例上。因此,如果所傳入的回調函數是普通函數,那麼函數內的this將指向bus實例,而不是註冊事件的那個組件實例:
...
bus.$on('move', function(pos){
this.pos = pos;
})
使用箭頭函數時不會有這個問題,它內部的this指向當前組件實例:
let temp = 123;
bus.$on('move', pos => {
this.pos = pos;
console.log(temp);
})
我在這裏特意定義了一個變量temp來說明問題。我們知道,通過作用域鏈,函數內可以訪問外部的變量temp。同樣的,由於箭頭函數不會重新定義this,因此函數內的變量this可以通過作用域鏈拿到外部this。
2. bus的使用
理解了bus模式的原理後,它的使用就非常簡單了。任何需要共享某些事件的組件只需引入同一個bus,就可以註冊和觸發共享的事件:
component1.vue
<template>
<button @click="handleClick">點擊我</button>
</template>
<script>
import bus from './bus.js';
export default {
methods: {
handleClick () {
bus.$emit('click');
}
},
mounted () {
bus.$on('move', pos => {
...
});
bus.$on('tick', payload => {
...
});
}
}
component2.vue
<template>
<div @mousemove.native="handleMove"></div>
</template>
<script>
export default {
methods: {
handleMove () {
bus.$emit('move');
}
},
mounted () {
bus.$on('click', () => {
...
})
}
}
用一張圖來表示上述關係:
bus允許任意多的組件參與到這個事件環路內,他們共享同一組事件隊列。
另外,如果事件關係很複雜,創建多個互不相關的bus也是可以的:
src
|-- bus
|-- clickBus.js
|-- moveBus.js
...
這裏我們創建了多個bus,clickBus專門用來管理點擊相關的事件,moveBus專門管理與移動相關的事件。需要註冊或觸發相應的事件時,引入對應的bus即可。
總結
一般來說,bus不應該被大規模用於項目中。因爲bus內事件的註冊和觸發分別位於不同的組件內,不便於跟蹤,這在一定程度上會帶來調試上的困難。
從實現原理上來說,Vuex的store模式其實是bus模式的一種封裝和變體,它也是藉助一個額外的Vue實例實現的:
不同的是,bus模式專注於事件管理,而store模式專注於數據管理。
一般來說,Vue推薦開發者多關注業務邏輯(即數據),事件應該由store直接管理,而不是發生在兩個不相關的組件之間。這也是爲什麼Vuex專注於數據管理,而不是事件管理。不過對我們來說,在恰當的時候使用bus模式,卻有可能收到意想不到的效果。