Vue 自定义事件

851 阅读4分钟

事件名

与组件和 prop 一样,事件名提供了自动的大小写转换。如果用驼峰命名的子组件中触发一个事件,你将可以在父组件中添加一个 kebab-case (短横线分隔命名) 的监听器。

this.$emit('myEvent')
<my-component @my-event="doSomething"></my-component>

与 props 的命名一样,当你使用 DOM 模板时,我们建议使用 kebab-case 事件监听器。如果你使用的是字符串模板,这个限制就不适用。

定义自定义事件

Vue 3.x

可以通过 emits 选项在组件上定义已发出的事件。

app.component('custom-form', {
  emits: ['inFocus', 'submit']
})

当在 emits 选项中定义了原生事件 (如 click) 时,将使用组件中的事件替代原生事件侦听器。

如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false。例如:禁用 attribute 继承的常见情况是需要将 attribute 应用于根节点之外的其他元素。

通过将 inheritAttrs 选项设置为 false,你可以访问组件的 $attrs property,该 property 包括组件 propsemits property 中未包含的所有属性 (例如,classstylev-on 监听器等)。

使用 date-picker 组件示例,如果需要将所有非 prop attribute 应用于 input 元素而不是根 div 元素,则可以使用 v-bind 缩写来完成。

app.component('base-input', {
  inheritAttrs: false,
  template: `
    <div class="base-input">
      <input type="datetime" v-bind="$attrs" />
    </div>
  `
})

有了这个新配置,data status attribute 将应用于 input 元素!

<!-- Date-picker 组件 使用非 prop attribute -->
<date-picker data-status="activated"></date-picker>

<!-- 渲染 date-picker 组件 -->
<div class="date-picker">
  <input type="datetime" data-status="activated" />
</div>

Vue 2.x

将原生事件绑定到组件

你可能有很多次想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on.native 修饰符:

<base-input v-on:focus.native="onFocus"></base-input>

在有的时候这是很有用的,不过在你尝试监听一个类似 <input> 的非常特定的元素时,这并不是个好主意。比如上述 <base-input> 组件可能做了如下重构,所以根元素实际上是一个 <label> 元素:

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit('input', $event.target.value)"
  >
</label>

这时,父级的 .native 监听器将静默失败。它不会产生任何报错,但是 onFocus 处理函数不会如你预期地被调用。为了解决这个问题,Vue 提供了一个 $listeners property,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:

{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

有了这个 $listeners property,你就可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 <input> 的你希望它也可以配合 v-model 工作的组件来说,为这些监听器创建一个类似下述 inputListeners 的计算属性通常是非常有用的:

Vue.component('base-input', {
  inheritAttrs: false, // required
  props: ['label', 'value'],
  computed: {
    inputListeners: function () {
      var vm = this
      // `Object.assign` 将所有的对象合并为一个新对象
      return Object.assign({},
        // 我们从父级添加所有的监听器
        this.$listeners,
        // 然后我们添加自定义监听器,
        // 或覆写一些监听器的行为
        {
          // 这里确保组件配合 `v-model` 的工作
          input: function (event) {
            vm.$emit('input', event.target.value)
          }
        }
      )
    }
  },
  template: `
    <label>
      {{ label }}
      <input
        v-bind="$attrs"
        v-bind:value="value"
        v-on="inputListeners"
      >
    </label>
  `
})

现在 <base-input> 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 <input> 元素一样使用了:所有跟它相同的 attribute监听器都可以工作,不必再使用 .native 监听器。

验证抛出的事件

与 prop 类型验证类似,如果使用对象语法而不是数组语法定义发出的事件,则可以验证它。

要添加验证,将为事件分配一个函数,该函数接收传递给 $emit 调用的参数,并返回一个布尔值以指示事件是否有效。

app.component('custom-form', {
  emits: {
    // 没有验证
    click: null,

    // 验证submit 事件
    submit: ({ email, password }) => {
      if (email && password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  },
  methods: {
    submitForm() {
      this.$emit('submit', { email, password })
    }
  }
})

v-model 参数

Vue 3.x

默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。我们可以通过向 v-model 传递参数(title)来修改这些标题:

<my-component v-model:title="bookTitle"></my-component>

在本例中,子组件将需要一个 title prop 并发出 update:title 要同步的事件:

app.component('my-component', {
  props: {
    title: String
  },
  emits: ['update:title'],
  template: `
    <input
      type="text"
      :value="title"
      @input="$emit('update:title', $event.target.value)">
  `
})
<my-component v-model:title="bookTitle"></my-component>

本例中,实现子组件与父组件属性值同步。另外,vue3删除了.sync

Vue 2.x

一个组件上的 v-model 默认会利用名为 valueprop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

现在在这个组件上使用 v-model 的时候:

<base-checkbox v-model="lovingVue"></base-checkbox>

这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新。

注意你仍然需要在组件的 props 选项里声明 checked 这个 prop。

多个 v-model 绑定

通过利用以特定 prop 和事件为目标的能力,正如我们之前在 v-model 参数中所学的那样,我们现在可以在单个组件实例上创建多个 v-model 绑定。

每个 v-model 将同步到不同的 prop,而不需要在组件中添加额外的选项:

<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>
app.component('user-name', {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName'],
  template: `
    <input 
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)">

    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)">
  `
})

处理 v-model 修饰符

在 2.x 中,我们对组件 v-model 上的 .trim 等修饰符提供了硬编码支持。但是,如果组件可以支持自定义修饰符,则会更有用。在 3.x 中,添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件:

当我们学习表单输入绑定时,我们看到 v-model 有内置修饰符——.trim、.number 和 .lazy。但是,在某些情况下,你可能还需要添加自己的自定义修饰符。

让我们创建一个示例自定义修饰符 capitalize,它将 v-model 绑定提供的字符串的第一个字母大写。

添加到组件 v-model 的修饰符将通过 modelModifiers prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers prop。

请注意,当组件的 created 生命周期钩子触发时,modelModifiers prop 会包含 capitalize,且其值为 true——因为 capitalize 被设置在了写为 v-model.capitalize="myText" 的 v-model 绑定上。

<my-component v-model.capitalize="myText"></my-component>
app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  template: `
    <input type="text"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)">
  `,
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
})

现在我们已经设置了 prop,我们可以检查 modelModifiers 对象键并编写一个处理器来更改发出的值。在下面的代码中,每当 元素触发 input 事件时,我们都将字符串大写。

<div id="app">
  <my-component v-model.capitalize="myText"></my-component>
  {{ myText }}
</div>
const app = Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
})

app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  },
  template: `<input
    type="text"
    :value="modelValue"
    @input="emitValue">`
})

app.mount('#app')

对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + "Modifiers":

<my-component v-model:description.capitalize="myText"></my-component>
app.component('my-component', {
  props: ['description', 'descriptionModifiers'],
  emits: ['update:description'],
  template: `
    <input type="text"
      :value="description"
      @input="$emit('update:description', $event.target.value)">
  `,
  created() {
    console.log(this.descriptionModifiers) // { capitalize: true }
  }
})