在Vue 2中采用了发布订阅模式,典型的兄弟组件间的通信$on和$emit
发布订阅模式:(订阅者、发布者、信号中心):一个发布者$emit发布一个事件到信号中心eventBus,订阅者们$on通过信号中心收到该事件,进行处理。注意:在 Vue 3 中,实例上的
$on方法已被移除,不再支持组件实例级别的事件订阅。
Vue.prototype.$emit的作用是循环执行当前 vm (组件实例)的 _events 属性内某个 event (事件名)对应的事件 回调 列表。也就是触发事件。
来看看源码,以下代码来自 vue@2.6.14.global.js
Vue.prototype.$emit = function (event) {
var vm = this;
{
var lowerCaseEvent = event.toLowerCase();
//...
}
var cbs = vm._events[event];
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
var args = toArray(arguments, 1);
var info = 'event handler for "' + event + '"';
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm;
};
而 vm 的 _events 属性内的对 event 的回调方法收集全部是通过Vue.prototype.$on方法收集的。即事件监听。
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm;
};
通过 $emit 实现父子组件通讯的第一个步骤:在父组件内,对子组件的占位符标签上绑定一个自定义事件回调。根据传入的事件名从当前实例的_events属性(即事件中心)中获取到该事件名所对应的回调函数cbs,然后再获取传入的附加参数args,由于cbs是一个数组,所以遍历该数组,拿到每一个回调函数,执行回调函数并将附加参数args传给该回调。这个绑定动作最终将通过子组件的 $on 方法将回调进行收集。
在这里模拟一个自定义事件 $on和$emit事件
//utils 新建里 event.js
export default {
map: {},
$emit(name, params) {
if (this.map[name] == null) {
console.log("没有找到关于" + name + "的事件,无法触发");
} else {
this.map[name].detail = params;
window.dispatchEvent(this.map[name]);
}
},
$on(name, work) {
let myEvent = new Event(name);
this.map[name] = myEvent;
window.addEventListener(name, event => {
console.log("得到数据为:", event.detail);
work(this.map[name].detail);
});
},
};
在main.js中注入(Vue3中使用eventBus.$on、eventBus.$emit功能)
import { createApp } from "vue";
import App from "./App.vue";
import myEvent from "./utils/event.js"; //引入
const app = createApp(App);
app.config.globalProperties.$event = myEvent; //绑定到全局中
app.mount("#app");
vue3中使用
import { getCurrentInstance, onMounted } from "vue";
const { proxy } = getCurrentInstance();
const $event = proxy.$event;
//以上为公共,每个页面都需要注册;
//child页面
const sendData = () => {
$event.$emit("changeStartMenu", 11111);
};
//根页面
onMounted(() => {
$event.$on("changeStartMenu", res => {
console.log(res, "触发成功");
});
});
补充:
getCurrentInstance只能在setup或 生命周期钩子 中调用。如需在
setup或 生命周期钩子外使用,请先在setup中调用getCurrentInstance()获取该实例后再使用。
Vue 3 中事件处理的最佳实践
由于 Vue 3 移除了实例上的 $on、$off 和 $once 方法,以下是几种推荐的事件处理方案:
1. 使用 mitt 库实现事件总线
mitt 是一个轻量级的事件总线库,适用于 Vue 3 中的组件通信:
// utils/eventBus.js
import mitt from "mitt";
const emitter = mitt();
export default emitter;
使用方式:
组件A:发布事件
import emitter from "./utils/eventBus";
export default {
setup() {
const sendData = () => {
emitter.emit("changeStartMenu", 11111);
};
return {
sendData,
};
},
};
组件B:订阅事件
import emitter from "./utils/eventBus";
import { onMounted, onUnmounted } from "vue";
export default {
setup() {
const handleEvent = data => {
console.log(data, "触发成功");
};
onMounted(() => {
emitter.on("changeStartMenu", handleEvent);
});
onUnmounted(() => {
emitter.off("changeStartMenu", handleEvent);
});
},
};
2. 使用 provide/inject 进行组件通信
对于层级较深的组件通信,可以使用 provide/inject:
父组件
import { provide, ref } from "vue";
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
provide("count", count);
provide("increment", increment);
return {};
},
};
子组件
import { inject } from "vue";
export default {
setup() {
const count = inject("count");
const increment = inject("increment");
return {
count,
increment,
};
},
};
3. 使用 Pinia 或 Vuex 进行状态管理
对于复杂的应用状态管理,推荐使用 Pinia(Vue 3 推荐)或 Vuex:
// stores/counter.js
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++;
},
},
});
使用方式:
import { useCounterStore } from "./stores/counter";
export default {
setup() {
const store = useCounterStore();
return {
count: store.count,
increment: store.increment,
};
},
};
4. 为什么 Vue 3 移除了 $on 方法?
Vue 3 移除实例级别的事件订阅方法主要有以下原因:
- 减少 API 复杂度:移除不常用的 API,使框架更加精简
- 避免内存泄漏:开发者可能忘记取消事件订阅,导致内存泄漏
- 鼓励使用更明确的通信方式:如 props、emit、provide/inject、状态管理库等
- 与组合式 API 更好地集成:组合式 API 提供了更灵活的响应式系统
通过以上方案,可以在 Vue 3 中实现更加清晰、可维护的组件通信方式。