子组件通过
$emit触发事件,向父组件发送消息。这是子组件与父组件通信的标准方式,与 props 形成完整的父子通信链路。
子组件使用 $emit 触发事件:
Vue.component('counter', {
template: '<button @click="increment">{{ count }}</button>',
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
this.$emit('increment')
}
}
})
父组件使用 v-on 监听事件:
<div id="app">
<counter @increment="handleIncrement"></counter>
<p>点击次数:{{ total }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
total: 0
},
methods: {
handleIncrement() {
this.total++
}
}
})
</script>
Vue.component('counter', {
template: '<button @click="increment">点击</button>',
methods: {
increment() {
this.$emit('increment', 10)
}
}
})
<counter @increment="handleIncrement"></counter>
<script>
new Vue({
methods: {
handleIncrement(value) {
console.log(value) // 10
}
}
})
</script>
Vue.component('user-card', {
props: ['user'],
template: '<button @click="select">选择用户</button>',
methods: {
select() {
this.$emit('select', this.user.id, this.user.name)
}
}
})
<user-card
:user="currentUser"
@select="handleSelect"
></user-card>
<script>
new Vue({
methods: {
handleSelect(id, name) {
console.log(id, name)
}
}
})
</script>
Vue.component('my-button', {
template: '<button @click="handleClick">点击</button>',
methods: {
handleClick(event) {
this.$emit('click', event)
}
}
})
<my-button @click="handleClick"></my-button>
// ✅ 推荐
this.$emit('my-event')
this.$emit('update-value')
// ❌ 不推荐(HTML 不区分大小写)
this.$emit('myEvent')
<!-- ✅ 推荐 -->
<my-component @my-event="handler"></my-component>
<!-- ❌ 不推荐 -->
<my-component @myEvent="handler"></my-component>
v-model 本质上是 props 和 events 的语法糖:
<input v-model="value">
<!-- 等同于 -->
<input :value="value" @input="value = $event">
Vue.component('my-input', {
props: ['value'],
template: `
<input
:value="value"
@input="$emit('input', $event.target.value)"
>
`
})
<my-input v-model="message"></my-input>
默认使用 value prop 和 input 事件,可以自定义:
Vue.component('my-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
:checked="checked"
@change="$emit('change', $event.target.checked)"
>
`
})
<my-checkbox v-model="isChecked"></my-checkbox>
.sync 是另一种双向绑定的语法糖:
<my-component :value.sync="value"></my-component>
<!-- 等同于 -->
<my-component
:value="value"
@update:value="value = $event"
></my-component>
Vue.component('my-component', {
props: ['value'],
template: `
<input
:value="value"
@input="$emit('update:value', $event.target.value)"
>
`
})
<user-form
:name.sync="user.name"
:email.sync="user.email"
></user-form>
Vue.component('user-form', {
props: ['name', 'email'],
template: `
<div>
<input
:value="name"
@input="$emit('update:name', $event.target.value)"
>
<input
:value="email"
@input="$emit('update:email', $event.target.value)"
>
</div>
`
})
Vue.component('alert', {
props: {
type: {
type: String,
default: 'info'
},
closable: {
type: Boolean,
default: true
}
},
data() {
return {
visible: true
}
},
template: `
<div v-if="visible" :class="['alert', 'alert-' + type]">
<slot></slot>
<button v-if="closable" @click="close">×</button>
</div>
`,
methods: {
close() {
this.visible = false
this.$emit('close')
}
}
})
<alert type="success" @close="handleClose">
操作成功!
</alert>
Vue.component('pagination', {
props: {
total: {
type: Number,
required: true
},
current: {
type: Number,
default: 1
},
pageSize: {
type: Number,
default: 10
}
},
computed: {
pages() {
return Math.ceil(this.total / this.pageSize)
}
},
template: `
<div class="pagination">
<button
:disabled="current === 1"
@click="$emit('change', current - 1)"
>上一页</button>
<button
v-for="page in pages"
:key="page"
:class="{ active: page === current }"
@click="$emit('change', page)"
>{{ page }}</button>
<button
:disabled="current === pages"
@click="$emit('change', current + 1)"
>下一页</button>
</div>
`
})
<pagination
:total="100"
:current="currentPage"
@change="handlePageChange"
></pagination>
Vue.component('search-form', {
data() {
return {
keyword: '',
category: ''
}
},
template: `
<form @submit.prevent="search">
<input v-model="keyword" placeholder="关键词">
<select v-model="category">
<option value="">全部分类</option>
<option value="1">分类一</option>
<option value="2">分类二</option>
</select>
<button type="submit">搜索</button>
</form>
`,
methods: {
search() {
this.$emit('search', {
keyword: this.keyword,
category: this.category
})
}
}
})
<search-form @search="handleSearch"></search-form>
Vue.component('file-upload', {
props: {
accept: String,
multiple: Boolean
},
template: `
<div class="file-upload">
<input
type="file"
:accept="accept"
:multiple="multiple"
@change="handleChange"
ref="input"
>
<button @click="$refs.input.click()">选择文件</button>
</div>
`,
methods: {
handleChange(event) {
const files = Array.from(event.target.files)
this.$emit('change', files)
files.forEach(file => {
this.$emit('file', file)
})
}
}
})
<file-upload
accept="image/*"
multiple
@file="handleFile"
@change="handleFiles"
></file-upload>
事件命名规范
// ✅ 推荐:使用短横线
this.$emit('update-user')
this.$emit('item-click')
// ❌ 不推荐:使用驼峰
this.$emit('updateUser') // HTML 中监听会出问题
事件参数解构
<!-- 使用 $event 获取第一个参数 -->
<my-component @event="handler($event, otherArg)"></my-component>
<!-- 使用箭头函数解构多个参数 -->
<my-component @event="(a, b) => handler(a, b)"></my-component>
自定义事件要点:
$emit 触发事件v-on 或 @ 监听事件$emit 的第二个参数开始是事件参数