1.11.5 自定义事件
我们知道,父组件使用 prop 传递数据给子组件。但子组件怎么跟父组件通信呢?这个时候 Vue 的自定义事件系统就派得上用场了。
使用v-on
绑定自定义事件
每个 Vue 实例都实现了事件接口,即:
- 使用
$on(eventName)
监听事件 - 使用
$emit(eventName, optionalPayload)
触发事件
Vue 的事件系统与浏览器的EventTarget API有所不同。尽管它们的运行起来类似,但是$on
和$emit
并不是addEventListener
和dispatchEvent
的别名。
另外,父组件可以在使用子组件的地方直接用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>
这个例子会为foo
和bar
同时添加用于更新的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 起是可配置的):
- 接受一个
value
prop - 在有新的值时触发
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
会使用value
prop 和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) { // ... })
在复杂的情况下,我们应该考虑使用专门的状态管理模式。