$emit方法和事件监听是构建交互式组件系统的关键。
组件事件是Vue中实现子组件向父组件通信的机制。子组件通过触发(emit)事件,父组件通过监听(listen)事件来实现组件间的数据流动。
触发事件$emit('event-name', data)
传递数据
事件冒泡
监听事件@event-name="handler"
父 → 子,单向数据流
子 → 父,反向通信
组合使用Props和事件
使用$emit方法触发事件,父组件使用v-on或@语法监听事件:
// ChildComponent.vue - 子组件
export default {
name: 'ChildComponent',
data() {
return {
message: '来自子组件的问候'
};
},
methods: {
sendMessage() {
// 触发自定义事件 'message-sent'
// 第一个参数:事件名称
// 第二个参数:传递的数据
this.$emit('message-sent', this.message);
},
sendCustomData() {
this.$emit('data-update', {
id: 1,
text: '自定义数据',
timestamp: new Date().toISOString()
});
}
},
template: `
<div class="child-component">
<button @click="sendMessage" class="btn btn-primary">
发送消息
</button>
<button @click="sendCustomData" class="btn btn-secondary">
发送数据
</button>
</div>
`
};
<!-- ParentComponent.vue - 父组件 -->
<template>
<div class="parent-component">
<h3>父组件</h3>
<p>接收到的消息: {{ receivedMessage }}</p>
<p>接收到的数据: {{ receivedData }}</p>
<!-- 监听子组件事件 -->
<child-component
@message-sent="handleMessage"
@data-update="handleDataUpdate"
/>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
data() {
return {
receivedMessage: '',
receivedData: null
};
},
methods: {
handleMessage(message) {
// 处理子组件传递的消息
this.receivedMessage = message;
console.log('收到消息:', message);
},
handleDataUpdate(data) {
// 处理子组件传递的数据
this.receivedData = data;
console.log('收到数据:', data);
// 可以在这里触发其他操作
this.processData(data);
},
processData(data) {
console.log('处理数据:', data.id, data.text);
}
}
};
</script>
Vue推荐使用kebab-case(短横线分隔)命名自定义事件:
| 推荐命名 | JavaScript中使用 | HTML模板中使用 |
|---|---|---|
user-selected |
this.$emit('user-selected', user) |
@user-selected="handleUserSelected" |
form-submitted |
this.$emit('form-submitted', formData) |
@form-submitted="handleSubmit" |
item-deleted |
this.$emit('item-deleted', itemId) |
@item-deleted="deleteItem" |
status-changed |
this.$emit('status-changed', newStatus) |
@status-changed="updateStatus" |
| 注意:与props不同,事件名称没有自动的大小写转换。 触发的事件名必须完全匹配监听时使用的名称。 | ||
可以向父组件传递多个参数,并可以在事件处理器中使用$event访问:
// 子组件:传递多个参数
methods: {
handleAction() {
// 传递多个参数
this.$emit('action-completed', 'success', Date.now(), {
userId: 123,
action: 'login'
});
}
}
<!-- 父组件:接收多个参数 -->
<!-- 方式1:使用内联处理器 -->
<child-component @action-completed="handleAction($event, 'extra')" />
<!-- 方式2:使用方法处理器 -->
<child-component @action-completed="handleAction" />
<script>
export default {
methods: {
// 接收所有参数
handleAction(status, timestamp, data, extraParam) {
console.log('状态:', status);
console.log('时间戳:', timestamp);
console.log('数据:', data);
console.log('额外参数:', extraParam); // 仅在方式1中有效
},
// 或使用展开运算符接收所有参数
handleAction(...args) {
const [status, timestamp, data] = args;
console.log('所有参数:', args);
}
}
}
</script>
Vue为组件事件提供了一些有用的修饰符:
<!-- 监听组件根元素的原生事件 -->
<my-component @click.native="handleClick"></my-component>
<!-- 注意:Vue 3中已移除.native修饰符 -->
<!-- Vue 3中使用v-bind="$attrs"或emits选项 -->
用途:监听组件根元素的原生DOM事件
<!-- 事件只触发一次 -->
<my-component @custom-event.once="handleOnce"></my-component>
用途:确保事件处理器只执行一次
<!-- 阻止事件默认行为 -->
<form @submit.prevent="handleSubmit">
<!-- 在组件中使用 -->
<custom-input @keydown.enter.prevent="submit" />
</form>
用途:阻止事件的默认行为
.sync修饰符是Vue提供的语法糖,用于实现父子组件的双向绑定:
<!-- 父组件使用.sync -->
<child-component :title.sync="pageTitle"></child-component>
<!-- 等价于 -->
<child-component
:title="pageTitle"
@update:title="pageTitle = $event"
></child-component>
// 子组件实现
export default {
props: {
title: String
},
methods: {
updateTitle(newTitle) {
// 触发约定格式的事件
this.$emit('update:title', newTitle);
}
}
}
.sync简化双向绑定逻辑update:propName格式.sync已被v-model参数替代使用model选项自定义组件的v-model行为:
// CustomInput.vue - 自定义输入组件
export default {
name: 'CustomInput',
// 自定义v-model
model: {
prop: 'value', // 绑定的prop名称
event: 'input' // 触发的事件名称
},
props: {
value: String, // 必须与model.prop一致
placeholder: String
},
methods: {
handleInput(event) {
// 触发input事件,更新v-model绑定的值
this.$emit('input', event.target.value);
}
},
template: `
<input
type="text"
:value="value"
:placeholder="placeholder"
@input="handleInput"
class="custom-input"
>
`
};
<!-- 使用自定义v-model -->
<custom-input v-model="username" placeholder="请输入用户名"></custom-input>
<!-- 等价于 -->
<custom-input
:value="username"
@input="username = $event"
placeholder="请输入用户名"
></custom-input>
创建一个完整的任务管理组件系统,展示组件事件的综合应用:
// TaskItem.vue - 任务项组件
export default {
name: 'TaskItem',
props: {
task: {
type: Object,
required: true,
validator: function(value) {
return value && value.id && value.text
}
},
completed: {
type: Boolean,
default: false
}
},
data() {
return {
isEditing: false,
editText: this.task.text
};
},
computed: {
statusClass() {
return {
'completed': this.completed,
'editing': this.isEditing
};
}
},
methods: {
toggleComplete() {
// 触发任务状态切换事件
this.$emit('toggle-complete', this.task.id);
},
startEdit() {
this.isEditing = true;
this.editText = this.task.text;
this.$emit('edit-start', this.task.id);
},
saveEdit() {
if (this.editText.trim()) {
// 触发任务更新事件
this.$emit('update-task', {
id: this.task.id,
text: this.editText.trim()
});
this.isEditing = false;
}
},
cancelEdit() {
this.isEditing = false;
this.$emit('edit-cancel', this.task.id);
},
deleteTask() {
// 触发任务删除事件
this.$emit('delete-task', this.task.id);
},
// 使用.sync修饰符的双向绑定
updateCompleted(newValue) {
this.$emit('update:completed', newValue);
}
},
template: `
<div class="task-item" :class="statusClass">
<div class="task-content">
<!-- 完成状态复选框 -->
<input
type="checkbox"
:checked="completed"
@change="updateCompleted(!completed)"
class="task-checkbox"
>
<!-- 显示模式 -->
<span v-if="!isEditing" class="task-text">
{{ task.text }}
</span>
<!-- 编辑模式 -->
<div v-else class="task-edit">
<input
type="text"
v-model="editText"
@keyup.enter="saveEdit"
@keyup.esc="cancelEdit"
class="form-control"
ref="editInput"
>
<button @click="saveEdit" class="btn btn-sm btn-success">
保存
</button>
<button @click="cancelEdit" class="btn btn-sm btn-secondary">
取消
</button>
</div>
</div>
<div class="task-actions" v-if="!isEditing">
<button @click="startEdit" class="btn btn-sm btn-outline-primary">
编辑
</button>
<button @click="deleteTask" class="btn btn-sm btn-outline-danger">
删除
</button>
</div>
</div>
`,
watch: {
isEditing(newVal) {
if (newVal) {
// 编辑模式下聚焦输入框
this.$nextTick(() => {
this.$refs.editInput?.focus();
});
}
}
}
};
// TaskList.vue - 任务列表组件
export default {
name: 'TaskList',
props: {
tasks: {
type: Array,
default: () => []
},
filter: {
type: String,
default: 'all', // all, active, completed
validator: value => ['all', 'active', 'completed'].includes(value)
}
},
computed: {
filteredTasks() {
switch (this.filter) {
case 'active':
return this.tasks.filter(task => !task.completed);
case 'completed':
return this.tasks.filter(task => task.completed);
default:
return this.tasks;
}
},
stats() {
const total = this.tasks.length;
const completed = this.tasks.filter(t => t.completed).length;
const active = total - completed;
return { total, completed, active };
}
},
methods: {
handleToggleComplete(taskId) {
this.$emit('toggle-complete', taskId);
},
handleUpdateTask(updatedTask) {
this.$emit('update-task', updatedTask);
},
handleDeleteTask(taskId) {
this.$emit('delete-task', taskId);
},
handleEditStart(taskId) {
this.$emit('edit-start', taskId);
},
handleEditCancel(taskId) {
this.$emit('edit-cancel', taskId);
}
},
template: `
<div class="task-list">
<!-- 统计信息 -->
<div class="task-stats">
<span class="badge bg-primary">全部: {{ stats.total }}</span>
<span class="badge bg-success">已完成: {{ stats.completed }}</span>
<span class="badge bg-warning">进行中: {{ stats.active }}</span>
</div>
<!-- 任务列表 -->
<div class="task-items">
<task-item
v-for="task in filteredTasks"
:key="task.id"
:task="task"
:completed.sync="task.completed"
@toggle-complete="handleToggleComplete"
@update-task="handleUpdateTask"
@delete-task="handleDeleteTask"
@edit-start="handleEditStart"
@edit-cancel="handleEditCancel"
/>
<!-- 空状态 -->
<div v-if="filteredTasks.length === 0" class="empty-state">
<template v-if="filter === 'all'">
暂无任务,添加一个吧!
</template>
<template v-else-if="filter === 'active'">
没有进行中的任务
</template>
<template v-else>
没有已完成的任务
</template>
</div>
</div>
<!-- 事件日志 -->
<div class="event-log" v-if="showEventLog">
<h6>事件日志</h6>
<pre>{{ eventLog }}</pre>
</div>
</div>
`
};
// 创建事件总线
const EventBus = new Vue();
// 组件A:发送事件
EventBus.$emit('global-event', data);
// 组件B:监听事件
EventBus.$on('global-event', (data) => {
console.log('收到全局事件:', data);
});
// 组件B:取消监听
EventBus.$off('global-event');
用途:非父子组件间的通信
// eventHub.js - 集中管理事件
export default {
events: {
USER_LOGIN: 'user-login',
DATA_LOADED: 'data-loaded',
ERROR_OCCURRED: 'error-occurred'
},
emit(event, ...args) {
this.$emit(event, ...args);
},
on(event, callback) {
this.$on(event, callback);
},
off(event, callback) {
this.$off(event, callback);
}
};
用途:大型应用中的事件管理
// 触发多个相关事件
methods: {
handleSave() {
// 触发主事件
this.$emit('save-started');
try {
// 执行保存逻辑
const result = this.performSave();
// 触发成功事件
this.$emit('save-success', result);
// 同时触发通用的保存事件
this.$emit('save-completed', {
success: true,
data: result
});
} catch (error) {
// 触发错误事件
this.$emit('save-error', error);
// 同时触发通用的保存事件
this.$emit('save-completed', {
success: false,
error: error
});
}
}
}
用途:复杂操作的事件序列
click、change.once修饰符可能原因:事件名称不匹配
解决方案:检查大小写,统一使用kebab-case
可能原因:未正确移除事件监听
解决方案:在beforeDestroy钩子中清理
可能原因:直接修改了prop数据
解决方案:始终通过事件通知父组件修改
可能原因:全局事件总线未清理
解决方案:组件销毁时移除所有事件监听
为以下购物车商品组件设计合适的事件:
// CartItem.vue - 购物车商品组件
Vue.component('CartItem', {
props: {
product: {
type: Object,
required: true
},
quantity: {
type: Number,
default: 1
}
},
// TODO: 定义需要触发的事件
// 考虑以下操作:
// 1. 数量变化
// 2. 删除商品
// 3. 收藏商品
// 4. 查看详情
template: `
<div class="cart-item">
<img :src="product.image" :alt="product.name">
<div class="product-info">
<h4>{{ product.name }}</h4>
<p>¥{{ product.price }}</p>
</div>
<div class="quantity-control">
<button @click="decrease">-</button>
<span>{{ quantity }}</span>
<button @click="increase">+</button>
</div>
<div class="actions">
<button @click="remove">删除</button>
<button @click="toggleFavorite">
{{ isFavorite ? '取消收藏' : '收藏' }}
</button>
<button @click="viewDetails">详情</button>
</div>
</div>
`
});