当前位置: 首页 > 文档资料 > Vue.js 教程 >

1.11.5 自定义事件

优质
小牛编辑
132浏览
2023-12-01

我们知道,父组件使用 prop 传递数据给子组件。但子组件怎么跟父组件通信呢?这个时候 Vue 的自定义事件系统就派得上用场了。

使用v-on绑定自定义事件

每个 Vue 实例都实现了事件接口,即:

  • 使用$on(eventName)监听事件
  • 使用$emit(eventName, optionalPayload)触发事件

Vue 的事件系统与浏览器的EventTarget API有所不同。尽管它们的运行起来类似,但是$on$emit并不是addEventListenerdispatchEvent的别名。

另外,父组件可以在使用子组件的地方直接用v-on来监听子组件触发的事件。

不能用$on监听子组件释放的事件,而必须在模板里直接用v-on绑定,参见下面的例子。

下面是一个例子:

<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
Vue.component('button-counter', {
  template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
  data: function () {
    return {
      counter: 0
    }
  },
  methods: {
    incrementCounter: function () {
      this.counter += 1
      this.$emit('increment')
    }
  },
})

new Vue({
  el: '#counter-event-example',
  data: {
    total: 0
  },
  methods: {
    incrementTotal: function () {
      this.total += 1
    }
  }
})

{{ total }}

在本例中,子组件已经和它外部完全解耦了。它所做的只是报告自己的内部事件,因为父组件可能会关心这些事件。请注意这一点很重要。

这里有一个如何使用载荷 (payload) 数据的示例:

<div id="message-event-example" class="demo">
  <p v-for="msg in messages">{{ msg }}</p>
  <button-message v-on:message="handleMessage"></button-message>
</div>
Vue.component('button-message', {
  template: `<div>
    <input type="text" v-model="message" />
    <button v-on:click="handleSendMessage">Send</button>
  </div>`,
  data: function () {
    return {
      message: 'test message'
    }
  },
  methods: {
    handleSendMessage: function () {
      this.$emit('message', { message: this.message })
    }
  }
})

new Vue({
  el: '#message-event-example',
  data: {
    messages: []
  },
  methods: {
    handleMessage: function (payload) {
      this.messages.push(payload.message)
    }
  }
})

{{ msg }}

第二个示例的重点在于子组件仍然是完全和外界解耦的。它做的事情全都是记录其自身的活动,活动记录是包括一份传入事件触发器的载荷数据在内的,只是为了展示父组件可以不关注的一个场景。

给组件绑定原生事件

有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用v-on的修饰符.native。例如:

<my-component v-on:click.native="doTheThing"></my-component>

.sync修饰符

2.3.0+

在一些情况下,我们可能会需要对一个 prop 进行“双向绑定”。事实上,这正是 Vue 1.x 中的.sync修饰符所提供的功能。当一个子组件改变了一个带.sync的 prop 的值时,这个变化也会同步到父组件中所绑定的值。这很方便,但也会导致问题,因为它破坏了单向数据流。由于子组件改变 prop 的代码和普通的状态改动代码毫无区别,当光看子组件的代码时,你完全不知道它何时悄悄地改变了父组件的状态。这在 debug 复杂结构的应用时会带来很高的维护成本。

上面所说的正是我们在 2.0 中移除.sync的理由。但是在 2.0 发布之后的实际应用中,我们发现.sync还是有其适用之处,比如在开发可复用的组件库时。我们需要做的只是让子组件改变父组件状态的代码更容易被区分。

从 2.3.0 起我们重新引入了.sync修饰符,但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的v-on监听器。

如下代码

<comp :foo.sync="bar"></comp>

会被扩展为:

<comp :foo="bar" @update:foo="val => bar = val"></comp>

当子组件需要更新foo的值时,它需要显式地触发一个更新事件:

this.$emit('update:foo', newValue)

当使用一个对象一次性设置多个属性的时候,这个.sync修饰符也可以和v-bind一起使用:

<comp v-bind.sync="{ foo: 1, bar: 2 }"></comp>

这个例子会为foobar同时添加用于更新的v-on监听器。

使用自定义事件的表单输入组件

自定义事件可以用来创建自定义的表单输入组件,使用v-model来进行数据双向绑定。要牢记:

<input v-model="something">

这不过是以下示例的语法糖:

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">

所以在组件中使用时,它相当于下面的简写:

<custom-input
  v-bind:value="something"
  v-on:input="something = arguments[0]">
</custom-input>

所以要让组件的v-model生效,它应该 (从 2.2.0 起是可配置的):

  • 接受一个valueprop
  • 在有新的值时触发input事件并将新值作为参数

我们来看一个非常简单的货币输入的自定义控件:

<currency-input v-model="price"></currency-input>
Vue.component('currency-input', {
  template: '\
    <span>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
      >\
    </span>\
  ',
  props: ['value'],
  methods: {
    // 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制
    updateValue: function (value) {
      var formattedValue = value
        // 删除两侧的空格符
        .trim()
        // 保留 2 位小数
        .slice(
          0,
          value.indexOf('.') === -1
            ? value.length
            : value.indexOf('.') + 3
        )
      // 如果值尚不合规,则手动覆盖为合规的值
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // 通过 input 事件带出数值
      this.$emit('input', Number(formattedValue))
    }
  }
})

自定义组件的v-model

2.2.0 新增

默认情况下,一个组件的v-model会使用valueprop 和input事件。但是诸如单选框、复选框之类的输入类型可能把value用作了别的目的。model选项可以避免这样的冲突:

Vue.component('my-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean,
    // 这样就允许拿 `value` 这个 prop 做其它事了
    value: String
  },
  // ...
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>

上述代码等价于:

<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>

注意你仍然需要显式声明checked这个 prop。

非父子组件的通信

有时候,非父子关系的两个组件之间也需要通信。在简单的场景下,可以使用一个空的 Vue 实例作为事件总线:

var bus = new Vue()
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
  // ...
})

在复杂的情况下,我们应该考虑使用专门的状态管理模式