vue将原生事件绑定到组件
# 背景
<div id="app">
<my-button @click='handleClick'></my-button>
</div>
Vue.component('my-button',{
template:`<button>点击</button>`
})
const vm = new Vue({
el:'#app',
methods:{
handleClick(){
console.log('click')
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
在这里,我们定义了my-button子组件,在父组件中引用了,然后我们想在上面绑定click事件,触发handleClick回调。
结果很容易猜到,没有触发,click没有打印输出。因为vue有自己事件运行机制,my-button不是原生DOM元素,我们是无法直接给其绑定原生事件并触发的
# 总括
总的来说,有三种方法:
- 使用native修饰符
- 使用$emit分发事件
- 使用$listeners
三种方法都能使原生事件绑定到组件上,就写法上当然是第一种最简单,第三种更麻烦。但是只要理解了就都挺好写的了。
但是在使用时,还是根据需求来,若是就是想绑定到组件的根标签上,直接使用第一种即可。否则,便使用二或三。
# 方案
# 使用native修饰符
- 加上.native修饰符后便不再是自定义事件,而是组件的原生事件;
- 此时便不再是由子组件向外触发的自定义事件,而是组件的原生点击事件;
- .native 原生修饰符的官方解释是: 监听组件根元素的原生事件!
仅需要在click后加上.native就可,如下
<div id="app">
<my-button @click.native='handleClick'></my-button>
</div>
2
3
相当于我们会把事件放在原生button标签上,此时事件便触发,click就打印了。
虽然这个方法使用起来非常简单,但是其存在局限性:它只会把事件放在子组件的根标签上。
上面子组件的根标签就是button,自然就触发了。
但是某些情况下,将某些事件绑定在根标签而非目标标签时,是无法触发事件的。如下情况:
<div id="app2">
<my-input @focus.native='handleFocus'></my-input>
</div>
Vue.component('my-input',{
template:`
<label for="">
label:
<input type="text">
</label>
`
})
const vm2 = new Vue({
el:'#app2',
methods:{
handleFocus(){
console.log('focus...')
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
尽管我们使用了native修饰符,但是focus事件放在子组件根标签——label标签上,无法触发该事件。
# 使用$emit分发事件
Vue.component('my-input',{
template:`
<label for="">
label:
<input type="text" @focus='$emit("focus","子组件的value")'>
</label>
`
})
const vm2 = new Vue({
el:'#app2',
methods:{
handleFocus(value){
console.log('focus...',value) //focus... 子组件的value
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在子组件input标签中绑定focus事件,其回调中使用$emit分发事件,使父组件事件触发。
$emit()有两个参数:
第一个参数为分发的事件名,在这里为focus,也可改别的,只需要与父组件中给子组件标签上绑定的事件名一致即可
第二个参数为给父组件该事件传的参数,我们在父组件中的该事件回调中就可接受到。所以我们一般想将子组件的数据传给父组件,完成父子组件间的通信,就可使用$emit。
除了这个解决方法外,还有第三种:使用$listeners
# 使用$listeners
$listeners 它是一个对象,里面包含了作用在这个组件上的所有监听器。
注意:使用.native修饰符的事件,不会体现在$listeners属性上。
Vue.component('my-input',{
template:`
<label for="">
label:
<input type="text" v-on='$listeners'>
</label>
`
})
2
3
4
5
6
7
8
(其余代码同上一个方法,故省略)
<div id="demo">
<child @click.native="handleChildClick" @blur="handleChildBlur"></child>
</div>
2
3
在input上使用 v-on="$listeners" ,就是将所有的事件监听器指向这个input元素。故也同样能打印出,且value值为event对象。
相比起方法二,这个$listener的使用更加全面
- 若是方法二,再在子组件标签上绑定多个事件,那就要在子组件进行相应的写事件名进行$emit分发;
- 而这个方法,就已经将所有事件监听绑在input元素上了,就不用再次设置;
- 而且,你也可以再次设置,$listeners的使用是很灵活的;
你可以自定义监听器,或者覆盖一些监听器的行为。
在下面的代码中,重写了focus事件监听器
Vue.component('my-input',{
template:`
<label for="">
label:
//<input type="text" v-on='$listeners'>
<input type="text" v-on='inputListeners'>
</label>
`,
computed:{
inputListeners(){
// Object.assign将所有对象合并为一个新的对象
// 从父级添加所有的的监听器
/*添加自定义监视器,或者复写一些监听器的行为,确保组件配合v-model的工作*/
return Object.assign({},this.$listeners,{
focus:(event)=>{
this.$emit('focus',event.target.value)
}
})
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
首先,我们得明白 v-on:xxx = fn 等价于 v-on={xxx:fn} (tips:这里不可写为@={xxx:fn}。所有指令的缩写 @ : # 等等,都是在其有参数的情况下使用)
其次,inputListeners是一个计算属性,返回的是一个对象,是将 $listeners 和 你重写的事件的对象 合并的对象
还有,在这里重写监听器,还是用到了$emit,每个监听器都得到event对象,我们可以取出event.target.value传给父组件
最后,父组件就能在focus时触发事件,并得到子组件传来的值了;
拓展:
当我们使用 $attrs 和 $listeners 时,my-input就相当于一个完全透明包裹器了。
- 前面我们已经知道了 v-on = $listeners 会把 所有父组件绑定到该子组件上的事件都放在该元素上
- 而使用 v-bind = $attrs 会把 除在props中声明了的,除style和class 的父传子的参数 都放在该元素上。
- 那么 当我们同时使用两者放到某个元素上时 ,就已经把父组件所有放在子组件标签(my-input)上的属性、事件, 全都放在了该元素上
- 此时,事件调用、属性获取都不再有障碍, my-input 不就可以理解成是透明的了嘛~
# $listeners
& $attrs
- $listeners,它是一个对象,里面包含了作用在这个组件上的所有监听器,你就可以配合 v-on="listeners" 将 所 有 的 事 件 监 听 器 指 向 这 个 组 件 的 某 个 特 定 的 子 元 素 ;
- 引用下vue的官方api中对listeners"将所有的事件监听器指向这个组件的某个特定的子元素;
- attrs包含了父作用域中不作为prop被识别(且获取)的特性绑定(class和style除外);
- listeners(自下而上传递数据), $attrs(自上而下多组件传递数据)
跨级通信方案:使用attrs和listeners实现祖孙组件之间的数据传递,也就是多重嵌套组件之间的数据传递;
$attrs是向下传递数据, $listeners 是向上传递方法,接受对象里的方法,来触发从父级接受来的函数。
# 相关链接
https://cn.vuejs.org/v2/guide/components-custom-events.html#%E5%B0%86%E5%8E%9F%E7%94%9F%E4%BA%8B%E4%BB%B6%E7%BB%91%E5%AE%9A%E5%88%B0%E7%BB%84%E4%BB%B6