自定义事件 $emit

子组件通过 $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 原理

v-model 本质上是 props 和 events 的语法糖:

<input v-model="value">

<!-- 等同于 -->
<input :value="value" @input="value = $event">

自定义组件的 v-model

Vue.component('my-input', {
  props: ['value'],
  template: `
    <input 
      :value="value"
      @input="$emit('input', $event.target.value)"
    >
  `
})
<my-input v-model="message"></my-input>

自定义 v-model 属性

默认使用 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 修饰符

.sync 是另一种双向绑定的语法糖:

<my-component :value.sync="value"></my-component>

<!-- 等同于 -->
<my-component 
  :value="value" 
  @update:value="value = $event"
></my-component>

实现 .sync

Vue.component('my-component', {
  props: ['value'],
  template: `
    <input 
      :value="value"
      @input="$emit('update:value', $event.target.value)"
    >
  `
})

多个 .sync

<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>

小结

自定义事件要点:

  1. 子组件触发:使用 $emit 触发事件
  2. 父组件监听:使用 v-on@ 监听事件
  3. 传递参数$emit 的第二个参数开始是事件参数
  4. 命名规范:使用短横线命名,避免大小写问题