Vue3移除了on、off和$once事件API,旨在简化API并更好地支持组合式API。
主要替代方案包括:
1)使用mitt等第三方事件库;
2)基于provide/inject和响应式状态模拟事件总线;
3)使用composable共享状态;
4)采用Pinia/Vuex进行状态管理;
5)父子组件通过props/emits通信。
Vue3鼓励使用更明确的组件通信方式,如状态管理工具和组合式API,替代传统事件总线模式,使代码更可维护。
推荐优先考虑Pinia配合provide/inject或composables方案。
组合式函数是Vue3的核心特性,支持逻辑复用、类型安全和清晰的作用域,适用于数据获取、表单处理等场景。
Vue 3 中移除了 $on、$off 和 $once 方法
这是 Vue 3 的一项重大变化。
为什么被移除?
Vue 3 的 事件 API 重构 旨在:
-
简化 API - 减少全局方法数量
-
更好地支持组合式 API - 传统的事件总线模式与组合式 API 理念不太契合
-
促进更明确的组件通信模式 - 鼓励使用更可维护的通信方式
替代方案
1. 使用第三方事件库(最接近的替代)
// 安装:npm install mitt
import mitt from 'mitt';
// 创建事件总线
const emitter = mitt();
// 发送事件
emitter.emit('event-name', data);
// 监听事件
emitter.on('event-name', (data) => {
console.log(data);
});
// 取消监听
emitter.off('event-name', handler);
2. 使用 Vue 3 的 provide/inject + 响应式状态
// 事件总线模拟
import { reactive, provide, inject } from 'vue';
// 创建事件总线
const eventBusSymbol = Symbol();
export function createEventBus() {
const listeners = reactive({});
return {
on(event, callback) {
if (!listeners[event]) listeners[event] = [];
listeners[event].push(callback);
},
off(event, callback) {
if (listeners[event]) {
const index = listeners[event].indexOf(callback);
if (index > -1) listeners[event].splice(index, 1);
}
},
emit(event, data) {
if (listeners[event]) {
listeners[event].forEach(callback => callback(data));
}
}
};
}
// 使用
export function useEventBus() {
return inject(eventBusSymbol);
}
3. 使用 composable 替代事件总线
// counter.js - 共享状态
import { ref, readonly } from 'vue';
export function useCounter() {
const count = ref(0);
const increment = () => {
count.value++;
// 可以在这里触发其他逻辑
};
return {
count: readonly(count),
increment
};
}
// 多个组件共享同一个实例
4. 使用 Vuex/Pinia 进行状态管理
// 使用 Pinia(Vue 3 推荐状态管理)
import { defineStore } from 'pinia';
export const useAppStore = defineStore('app', {
state: () => ({
events: [],
data: null
}),
actions: {
triggerEvent(eventName, payload) {
// 处理事件逻辑
}
}
});
5. 使用 v-model 和 emits(父子组件通信)
<!-- 子组件 -->
<script setup>
defineProps(['modelValue']);
defineEmits(['update:modelValue']);
const updateValue = (newVal) => {
emit('update:modelValue', newVal);
};
</script>
<!-- 父组件 -->
<ChildComponent v-model="value" />
迁移建议
-
父子组件通信 → 使用
props和emits -
兄弟组件通信 → 使用共同的父组件或状态管理
-
远房组件通信 → 使用 Pinia/Vuex 或 provide/inject
-
全局事件 → 使用
mitt等第三方库
总结
虽然 $on 被移除,但 Vue 3 提供了更现代化、类型安全的替代方案。
对于大多数应用,推荐使用 Pinia 进行状态管理,配合 provide/inject 或 composables 来替代传统的事件总线模式。
关联阅读推荐
Pinia 使用详解(附:如何查看或区分项目使用的是 Pinia 还是 Vuex 4)
Composables 是什么?
Composables(组合式函数) 是 Vue 3 组合式 API 的核心概念,是一种利用 Vue 组合式 API 来封装和重用有状态逻辑的函数。
核心特点
1. 逻辑复用
将组件逻辑提取到可复用的函数中:
javascript
// 使用 composable
import { useMouse } from './mouse';
export default {
setup() {
const { x, y } = useMouse();
return { x, y };
}
}
2. 有状态逻辑
不同于工具函数,composables 可以包含响应式状态:
javascript
// useCounter.js
import { ref } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => count.value++;
const decrement = () => count.value--;
const reset = () => count.value = initialValue;
return {
count,
increment,
decrement,
reset
};
}
完整示例
基础示例:鼠标位置跟踪
javascript
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
const update = (event) => {
x.value = event.pageX;
y.value = event.pageY;
};
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
在组件中使用
vue
<script setup>
import { useMouse } from './useMouse';
const { x, y } = useMouse();
</script>
<template>
<div>鼠标位置: {{ x }}, {{ y }}</div>
</template>
实际应用场景
1. 数据获取
javascript
// useFetch.js
import { ref, onMounted } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
const fetchData = async () => {
loading.value = true;
try {
const response = await fetch(url);
data.value = await response.json();
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};
onMounted(fetchData);
return {
data,
error,
loading,
refetch: fetchData
};
}
// 使用
const { data: users, loading } = useFetch('/api/users');
2. 本地存储
javascript
// useLocalStorage.js
import { ref, watch } from 'vue';
export function useLocalStorage(key, defaultValue) {
const data = ref(JSON.parse(localStorage.getItem(key)) || defaultValue);
watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
}, { deep: true });
const clear = () => {
localStorage.removeItem(key);
data.value = defaultValue;
};
return { data, clear };
}
// 使用
const { data: theme, clear } = useLocalStorage('theme', 'light');
3. 表单处理
javascript
// useForm.js
import { ref, computed } from 'vue';
export function useForm(initialValues = {}) {
const form = ref({ ...initialValues });
const errors = ref({});
const validate = () => {
// 验证逻辑
const newErrors = {};
if (!form.value.name) newErrors.name = '姓名必填';
if (!form.value.email.includes('@')) newErrors.email = '邮箱格式错误';
errors.value = newErrors;
return Object.keys(newErrors).length === 0;
};
const reset = () => {
form.value = { ...initialValues };
errors.value = {};
};
const isDirty = computed(() =>
JSON.stringify(form.value) !== JSON.stringify(initialValues)
);
return {
form,
errors,
validate,
reset,
isDirty
};
}
Composables vs 其他模式
| 特性 | Composables | Mixins | 高阶组件 | 工具函数 |
|---|---|---|---|---|
| 状态支持 | ✅ 响应式状态 | ✅ 有状态 | ⚠️ 复杂 | ❌ 无状态 |
| 逻辑隔离 | ✅ 明确来源 | ❌ 命名冲突 | ✅ 较好 | ✅ 明确 |
| TypeScript | ✅ 完美支持 | ⚠️ 一般 | ⚠️ 一般 | ✅ 支持 |
| 可组合性 | ✅ 易于组合 | ❌ 易冲突 | ⚠️ 嵌套深 | ✅ 可组合 |
最佳实践
1. 命名约定
javascript
// 以 "use" 开头
useUser()
useFetch()
useLocalStorage()
useMousePosition()
2. 参数灵活性
javascript
// 支持配置选项
export function useFetch(url, options = {}) {
const {
immediate = true,
retry = 3,
onSuccess = null
} = options;
// ...
}
3. 返回值一致性
javascript
// 返回响应式引用和函数
export function useTimer() {
const seconds = ref(0);
const isRunning = ref(false);
const start = () => { /* ... */ };
const stop = () => { /* ... */ };
return {
seconds,
isRunning,
start,
stop
};
}
4. 副作用管理
javascript
import { onUnmounted } from 'vue';
export function useEventListener(target, event, callback) {
onMounted(() => target.addEventListener(event, callback));
onUnmounted(() => target.removeEventListener(event, callback));
}
与事件总线的结合
javascript
// useEventBus.js - 用 composable 包装事件总线
import { onUnmounted } from 'vue';
import mitt from 'mitt';
const emitter = mitt();
export function useEventBus() {
const eventHandlers = [];
const emit = (event, data) => emitter.emit(event, data);
const on = (event, handler) => {
emitter.on(event, handler);
eventHandlers.push({ event, handler });
};
onUnmounted(() => {
eventHandlers.forEach(({ event, handler }) => {
emitter.off(event, handler);
});
});
return { emit, on };
}
// 使用
const { emit, on } = useEventBus();
on('user-login', (user) => console.log(user));
emit('user-login', { name: 'John' });
总结
Composables 的本质:将 Vue 3 的响应式系统(ref、reactive)和生命周期钩子组合成可复用的逻辑单元。
优势:
-
✅ 逻辑复用 - 提取和共享状态逻辑
-
✅ 类型安全 - 完整的 TypeScript 支持
-
✅ 可组合 - 多个 composables 可以组合使用
-
✅ 作用域清晰 - 避免 mixin 的命名冲突问题
-
✅ 灵活 - 可以接受参数,返回需要的内容
适用场景:
-
数据获取和缓存
-
表单处理和验证
-
浏览器 API 交互
-
第三方库集成
-
复杂业务逻辑封装
这是 Vue 3 组合式 API 最强大的特性之一,极大地改善了代码的组织和复用方式。