关于 v-on 的 native 修饰符

下个套

native 这个修饰符我平时很少用到,但是在公司项目中遇到过,那就得把它弄清楚。

为了把它和常规的 v-on 进行比较,我准备了一个很简单的组件:

1
2
3
4
5
6
7
<template>
<div>123</div>
</template>

<script>
export default {}
</script>

这个组件里面什么都没有,先把它叫做 Test,那么现在有个父组件里面用到了 Test,<Test @click="handleClick" />,理想情况是点击 Test 之后打印出 123,那么实际结果是什么呢?

解答

如果你觉得能打印出 123,那么我推荐你去看一下我之前写的《对 Vue 事件机制错误理解的纠正》,实际上 Test 被点击后是没有任何反应的。原因在于 Vue 中事件的回调和触发都是需要你自己定义的,说具体点就是回调需要通过 v-on 提供,与此同时,在给组件设置事件的时候,触发条件也得你来设置

比如在刚刚的例子中,父组件给子组件的 click 事件设置了一个回调 handleClick,然而子组件里面根本没有触发这个事件,所以回调没有被调用(为什么不写成点击无效),要实现这个需求,正确的写法是:

1
2
3
4
5
6
7
<template>
<div @click="$emit('click')">123</div>
</template>

<script>
export default {}
</script>

这里监听了 div 的点击操作,在点击后让 Test 本身调用 \$emit(不理解 \$emit 运行机制的也可以参考《对 Vue 事件机制错误理解的纠正》),去执行父组件传来的回调函数 handleClick。

为什么不写成点击无效

从刚刚的例子可以看出,组件事件回调的设置者是你,触发的设置者也是你,实际上 @click 和点击操作已经没有直接联系了,比如说你可以写成 @input="handler",在子组件中使用 @click="$emit('input')",这也能完成点击执行 handler 的操作,例子中只是触发了一个叫做 click 的回调函数罢了

事实上,组件的事件名称是可以自定义的,只要让他在合适的时机触发就行了,当然了,实际开发环境中不建议乱起名字。

绕回来了

那么,native 修饰符有什么用呢,v-on 本身都那么灵活了,还需要修饰符吗?native 的作用是为组件的根元素绑定原生事件,注意是根元素,还是刚刚的例子,要做一个点击打印的效果,如果直接用 native,那么可以写成:

1
2
3
4
5
6
7
8
9
10
11
<Test @click.native="handleClick" />

<!-- 和 -->

<template>
<div>123</div>
</template>

<script>
export default {}
</script>

区别就出来了,子组件里面什么事件相关的内容都没有,但仍然能对点击操作做出反馈,因为父组件给子组件的根元素(也就是 div),绑定了个原生的点击事件

要注意的是,这里绑的是原生事件,如果把 `@click.native改成@agssff.native` 这种瞎打的事件名,是一点效果都没有的,因为它不是原生事件

那突发奇想,在子组件里调用 $emit('agssff') 呢?实际上也是没有效果的,因为 $emit 的运行机制,他会调用储存在 events 里的回调函数,native 将原生事件直接绑定在了根元素上,根本没有通过 events,所以 $emit 也是无效的。

举一反三

对 Vue 的事件原理有了进一步理解之后,可以更好地解决一些实际问题了,比如跨组件传递 v-model

现在有一个父组件 Parent 和子组件 Children,Children中有一个 input 元素,需要在父组件中通过 v-model 关联这个 input 元素,尽可能不要在子组件中引入新的响应式变量

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<Children v-model="text"></Children>
</template>

<script>
export default {
data () {
return {
text: 'Hello world'
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
<template>
<input type="text" :value="text" input="$emit('input', $event.target.value)">
</template>

<script>
export default {
props: {
text: String
}
}
</script>

这就是完美的解法,父组件的 v-model 是一个由 v-bindv-on 构成的语法糖,其中 v-on 向子组件的 events 中添加了一个名为 input 的函数,所以子组件可以直接通过 $emit('input') 来触发这个函数。

如果直接在 input 元素中写 v-model="text",会造成子组件修改父组件数据,这明显是不正确的。

还有一种常见的写法,是在子组件中设置一个中间变量,比如 value,使 value 的初始值等同于 text,再用 v-model="value" 做双向绑定,watch 监听 value变化……

这明显就把问题变得更加复杂了。