Vue.js 3 事件总线
Vue.js 3 Event Bus
如何在 Vue 3 中创建事件总线?
在 Vue 2 中,它是:
export const bus = new Vue();
bus.$on(...)
bus.$emit(...)
在 Vue 3 中,Vue
不再是构造函数,Vue.createApp({});
returns 是一个没有 $on
和 $emit
方法的对象。
按照官方 docs you could use mitt 库中的建议在组件之间调度事件,假设我们有一个侧边栏和 header
其中包含一个 close/open 侧边栏的按钮,我们需要它按钮切换侧边栏组件内的一些 属性 :
在 main.js 中导入该库并创建该发射器的实例并定义为 global property:
安装:
npm install --save mitt
用法:
import { createApp } from 'vue'
import App from './App.vue'
import mitt from 'mitt';
const emitter = mitt();
const app = createApp(App);
app.config.globalProperties.emitter = emitter;
app.mount('#app');
在 header 中发出带有一些负载的 toggle-sidebar
事件:
<template>
<header>
<button @click="toggleSidebar"/>toggle</button>
</header>
</template>
<script >
export default {
data() {
return {
sidebarOpen: true
};
},
methods: {
toggleSidebar() {
this.sidebarOpen = !this.sidebarOpen;
this.emitter.emit("toggle-sidebar", this.sidebarOpen);
}
}
};
</script>
在边栏中接收带有负载的事件:
<template>
<aside class="sidebar" :class="{'sidebar--toggled': !isOpen}">
....
</aside>
</template>
<script>
export default {
name: "sidebar",
data() {
return {
isOpen: true
};
},
mounted() {
this.emitter.on("toggle-sidebar", isOpen => {
this.isOpen = isOpen;
});
}
};
</script>
对于那些使用组合 api 的人,他们可以使用 emitter
如下:
创建文件src/composables/useEmitter.js
import { getCurrentInstance } from 'vue'
export default function useEmitter() {
const internalInstance = getCurrentInstance();
const emitter = internalInstance.appContext.config.globalProperties.emitter;
return emitter;
}
然后您可以像使用 useRouter
:
一样使用 useEmitter
import useEmitter from '@/composables/useEmitter'
export default {
setup() {
const emitter = useEmitter()
...
}
...
}
使用组合API
您还可以利用新组合 API 并定义可组合的事件总线:
eventBus.js
import { ref } from "vue";
const bus = ref(new Map());
export default function useEventsBus(){
function emit(event, ...args) {
bus.value.set(event, args);
}
return {
emit,
bus
}
}
在组件 A 中执行:
import useEventsBus from './eventBus';
...
//in script setup or inside the setup hook
const {emit}=useEventsBus()
...
emit('sidebarCollapsed',val)
在组件 B 中:
const { bus } = useEventsBus()
watch(()=>bus.value.get('sidebarCollapsed'), (val) => {
// destruct the parameters
const [sidebarCollapsedBus] = val ?? []
sidebarCollapsed.value = sidebarCollapsedBus
})
在 Vue.js 的第 3 版中,您可以使用 third-party 库,或使用 publisher-subscriber(PubSub 概念)编程模式中编写的功能。
event.js
//events - a super-basic Javascript (publish subscribe) pattern
class Event{
constructor(){
this.events = {};
}
on(eventName, fn) {
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(fn);
}
off(eventName, fn) {
if (this.events[eventName]) {
for (var i = 0; i < this.events[eventName].length; i++) {
if (this.events[eventName][i] === fn) {
this.events[eventName].splice(i, 1);
break;
}
};
}
}
trigger(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(function(fn) {
fn(data);
});
}
}
}
export default new Event();
index.js
import Vue from 'vue';
import $bus from '.../event.js';
const app = Vue.createApp({})
app.config.globalProperties.$bus = $bus;
使用 Vue 组合和 defineEmit 你甚至可以让它更简单 :
<!-- Parent -->
<script setup>
import { defineEmit } from 'vue'
const emit = defineEmit(['selected'])
const onEmit = (data) => console.log(data)
</script>
<template>
<btnList
v-for="x in y"
:key="x"
:emit="emit"
@selected="onEmit"
/>
</template>
<!-- Children (BtnList.vue) -->
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
emit: Function
})
</script>
<template>
<button v-for="x in 10" :key="x" @click="props.emit('selected', x)">Click {{ x }}</button>
</template>
我只是用一个 children 展示了它,但您可以将 emit 函数传递给其他 children.
EventBus 的内容class 文件:
class EventBusEvent extends Event {
public data: any
constructor({type, data} : {type: string, data: any}) {
super(type)
this.data = data
}
}
class EventBus extends EventTarget {
private static _instance: EventBus
public static getInstance() : EventBus {
if (!this._instance) this._instance = new EventBus()
return this._instance
}
public emit(type : string, data?: any) : void {
this.dispatchEvent(new EventBusEvent({type, data}))
}
}
export default EventBus.getInstance()
在项目中的使用,发出事件:
import EventBus from '...path to eventbus file with class'
//...bla bla bla... code...
EventBus.emit('event type', {..some data..}')
监听事件:
import EventBus from '...path to eventbus file with class'
//...bla bla bla... code...
EventBus.addEventListener('event type', (event) => { console.log(event.data) })
我已将 改编为具有与 Vue 实例等效的接口,以便该实用程序可以用作 drop-in 替代品,不需要更改使用代码。
此版本还支持 $off
方法,第一个参数是事件名称数组。它还避免了 $off
方法中的一个问题,即 de-registering 多个事件侦听器实际上会删除一个错误的事件侦听器,因为在正向删除数组的同时也会从中删除项目。
event-bus.js
:
// @ts-check
/**
* Replacement for the Vue 2-based EventBus.
*
* @template EventName
*/
class Bus {
constructor() {
/**
* @type {Map<EventName, Array<{ callback: Function, once: boolean }>>}
*/
this.eventListeners = new Map()
}
/**
* @param {EventName} eventName
* @param {Function} callback
* @param {boolean} [once]
* @private
*/
registerEventListener(eventName, callback, once = false) {
if (!this.eventListeners.has(eventName)) {
this.eventListeners.set(eventName, [])
}
const eventListeners = this.eventListeners.get(eventName)
eventListeners.push({ callback, once })
}
/**
* See: https://v2.vuejs.org/v2/api/#vm-on
*
* @param {EventName} eventName
* @param {Function} callback
*/
$on(eventName, callback) {
this.registerEventListener(eventName, callback)
}
/**
* See: https://v2.vuejs.org/v2/api/#vm-once
*
* @param {EventName} eventName
* @param {Function} callback
*/
$once(eventName, callback) {
const once = true
this.registerEventListener(eventName, callback, once)
}
/**
* Removes all event listeners for the given event name or names.
*
* When provided with a callback function, removes only event listeners matching the provided function.
*
* See: https://v2.vuejs.org/v2/api/#vm-off
*
* @param {EventName | EventName[]} eventNameOrNames
* @param {Function} [callback]
*/
$off(eventNameOrNames, callback = undefined) {
const eventNames = Array.isArray(eventNameOrNames) ? eventNameOrNames : [eventNameOrNames]
for (const eventName of eventNames) {
const eventListeners = this.eventListeners.get(eventName)
if (eventListeners === undefined) {
continue
}
if (typeof callback === 'function') {
for (let i = eventListeners.length - 1; i >= 0; i--) {
if (eventListeners[i].callback === callback) {
eventListeners.splice(i, 1)
}
}
} else {
this.eventListeners.delete(eventName)
}
}
}
/**
* See: https://v2.vuejs.org/v2/api/#vm-emit
*
* @param {EventName} eventName
* @param {any} args
*/
$emit(eventName, ...args) {
if (!this.eventListeners.has(eventName)) {
return
}
const eventListeners = this.eventListeners.get(eventName)
const eventListenerIndexesToDelete = []
for (const [eventListenerIndex, eventListener] of eventListeners.entries()) {
eventListener.callback(...args)
if (eventListener.once) {
eventListenerIndexesToDelete.push(eventListenerIndex)
}
}
for (let i = eventListenerIndexesToDelete.length - 1; i >= 0; i--) {
eventListeners.splice(eventListenerIndexesToDelete[i], 1)
}
}
}
const EventBus = new Bus()
export default EventBus
old-event-bus.js
:
import Vue from 'vue'
const EventBus = new Vue()
export default EventBus
example.js
:
// import EventBus from './old-event-bus.js'
import EventBus from './event-bus.js'
如何在 Vue 3 中创建事件总线?
在 Vue 2 中,它是:
export const bus = new Vue();
bus.$on(...)
bus.$emit(...)
在 Vue 3 中,Vue
不再是构造函数,Vue.createApp({});
returns 是一个没有 $on
和 $emit
方法的对象。
按照官方 docs you could use mitt 库中的建议在组件之间调度事件,假设我们有一个侧边栏和 header
其中包含一个 close/open 侧边栏的按钮,我们需要它按钮切换侧边栏组件内的一些 属性 :
在 main.js 中导入该库并创建该发射器的实例并定义为 global property:
安装:
npm install --save mitt
用法:
import { createApp } from 'vue'
import App from './App.vue'
import mitt from 'mitt';
const emitter = mitt();
const app = createApp(App);
app.config.globalProperties.emitter = emitter;
app.mount('#app');
在 header 中发出带有一些负载的 toggle-sidebar
事件:
<template>
<header>
<button @click="toggleSidebar"/>toggle</button>
</header>
</template>
<script >
export default {
data() {
return {
sidebarOpen: true
};
},
methods: {
toggleSidebar() {
this.sidebarOpen = !this.sidebarOpen;
this.emitter.emit("toggle-sidebar", this.sidebarOpen);
}
}
};
</script>
在边栏中接收带有负载的事件:
<template>
<aside class="sidebar" :class="{'sidebar--toggled': !isOpen}">
....
</aside>
</template>
<script>
export default {
name: "sidebar",
data() {
return {
isOpen: true
};
},
mounted() {
this.emitter.on("toggle-sidebar", isOpen => {
this.isOpen = isOpen;
});
}
};
</script>
对于那些使用组合 api 的人,他们可以使用 emitter
如下:
创建文件src/composables/useEmitter.js
import { getCurrentInstance } from 'vue'
export default function useEmitter() {
const internalInstance = getCurrentInstance();
const emitter = internalInstance.appContext.config.globalProperties.emitter;
return emitter;
}
然后您可以像使用 useRouter
:
useEmitter
import useEmitter from '@/composables/useEmitter'
export default {
setup() {
const emitter = useEmitter()
...
}
...
}
使用组合API
您还可以利用新组合 API 并定义可组合的事件总线:
eventBus.js
import { ref } from "vue";
const bus = ref(new Map());
export default function useEventsBus(){
function emit(event, ...args) {
bus.value.set(event, args);
}
return {
emit,
bus
}
}
在组件 A 中执行:
import useEventsBus from './eventBus';
...
//in script setup or inside the setup hook
const {emit}=useEventsBus()
...
emit('sidebarCollapsed',val)
在组件 B 中:
const { bus } = useEventsBus()
watch(()=>bus.value.get('sidebarCollapsed'), (val) => {
// destruct the parameters
const [sidebarCollapsedBus] = val ?? []
sidebarCollapsed.value = sidebarCollapsedBus
})
在 Vue.js 的第 3 版中,您可以使用 third-party 库,或使用 publisher-subscriber(PubSub 概念)编程模式中编写的功能。
event.js
//events - a super-basic Javascript (publish subscribe) pattern
class Event{
constructor(){
this.events = {};
}
on(eventName, fn) {
this.events[eventName] = this.events[eventName] || [];
this.events[eventName].push(fn);
}
off(eventName, fn) {
if (this.events[eventName]) {
for (var i = 0; i < this.events[eventName].length; i++) {
if (this.events[eventName][i] === fn) {
this.events[eventName].splice(i, 1);
break;
}
};
}
}
trigger(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(function(fn) {
fn(data);
});
}
}
}
export default new Event();
index.js
import Vue from 'vue';
import $bus from '.../event.js';
const app = Vue.createApp({})
app.config.globalProperties.$bus = $bus;
使用 Vue 组合和 defineEmit 你甚至可以让它更简单 :
<!-- Parent -->
<script setup>
import { defineEmit } from 'vue'
const emit = defineEmit(['selected'])
const onEmit = (data) => console.log(data)
</script>
<template>
<btnList
v-for="x in y"
:key="x"
:emit="emit"
@selected="onEmit"
/>
</template>
<!-- Children (BtnList.vue) -->
<script setup>
import { defineProps } from 'vue'
const props = defineProps({
emit: Function
})
</script>
<template>
<button v-for="x in 10" :key="x" @click="props.emit('selected', x)">Click {{ x }}</button>
</template>
我只是用一个 children 展示了它,但您可以将 emit 函数传递给其他 children.
EventBus 的内容class 文件:
class EventBusEvent extends Event {
public data: any
constructor({type, data} : {type: string, data: any}) {
super(type)
this.data = data
}
}
class EventBus extends EventTarget {
private static _instance: EventBus
public static getInstance() : EventBus {
if (!this._instance) this._instance = new EventBus()
return this._instance
}
public emit(type : string, data?: any) : void {
this.dispatchEvent(new EventBusEvent({type, data}))
}
}
export default EventBus.getInstance()
在项目中的使用,发出事件:
import EventBus from '...path to eventbus file with class'
//...bla bla bla... code...
EventBus.emit('event type', {..some data..}')
监听事件:
import EventBus from '...path to eventbus file with class'
//...bla bla bla... code...
EventBus.addEventListener('event type', (event) => { console.log(event.data) })
我已将
此版本还支持 $off
方法,第一个参数是事件名称数组。它还避免了 $off
方法中的一个问题,即 de-registering 多个事件侦听器实际上会删除一个错误的事件侦听器,因为在正向删除数组的同时也会从中删除项目。
event-bus.js
:
// @ts-check
/**
* Replacement for the Vue 2-based EventBus.
*
* @template EventName
*/
class Bus {
constructor() {
/**
* @type {Map<EventName, Array<{ callback: Function, once: boolean }>>}
*/
this.eventListeners = new Map()
}
/**
* @param {EventName} eventName
* @param {Function} callback
* @param {boolean} [once]
* @private
*/
registerEventListener(eventName, callback, once = false) {
if (!this.eventListeners.has(eventName)) {
this.eventListeners.set(eventName, [])
}
const eventListeners = this.eventListeners.get(eventName)
eventListeners.push({ callback, once })
}
/**
* See: https://v2.vuejs.org/v2/api/#vm-on
*
* @param {EventName} eventName
* @param {Function} callback
*/
$on(eventName, callback) {
this.registerEventListener(eventName, callback)
}
/**
* See: https://v2.vuejs.org/v2/api/#vm-once
*
* @param {EventName} eventName
* @param {Function} callback
*/
$once(eventName, callback) {
const once = true
this.registerEventListener(eventName, callback, once)
}
/**
* Removes all event listeners for the given event name or names.
*
* When provided with a callback function, removes only event listeners matching the provided function.
*
* See: https://v2.vuejs.org/v2/api/#vm-off
*
* @param {EventName | EventName[]} eventNameOrNames
* @param {Function} [callback]
*/
$off(eventNameOrNames, callback = undefined) {
const eventNames = Array.isArray(eventNameOrNames) ? eventNameOrNames : [eventNameOrNames]
for (const eventName of eventNames) {
const eventListeners = this.eventListeners.get(eventName)
if (eventListeners === undefined) {
continue
}
if (typeof callback === 'function') {
for (let i = eventListeners.length - 1; i >= 0; i--) {
if (eventListeners[i].callback === callback) {
eventListeners.splice(i, 1)
}
}
} else {
this.eventListeners.delete(eventName)
}
}
}
/**
* See: https://v2.vuejs.org/v2/api/#vm-emit
*
* @param {EventName} eventName
* @param {any} args
*/
$emit(eventName, ...args) {
if (!this.eventListeners.has(eventName)) {
return
}
const eventListeners = this.eventListeners.get(eventName)
const eventListenerIndexesToDelete = []
for (const [eventListenerIndex, eventListener] of eventListeners.entries()) {
eventListener.callback(...args)
if (eventListener.once) {
eventListenerIndexesToDelete.push(eventListenerIndex)
}
}
for (let i = eventListenerIndexesToDelete.length - 1; i >= 0; i--) {
eventListeners.splice(eventListenerIndexesToDelete[i], 1)
}
}
}
const EventBus = new Bus()
export default EventBus
old-event-bus.js
:
import Vue from 'vue'
const EventBus = new Vue()
export default EventBus
example.js
:
// import EventBus from './old-event-bus.js'
import EventBus from './event-bus.js'