夜间模式CodeSnippetStyle:
日间模式CodeSnippetStyle:

0%


[Vue深入组件]:native事件修饰符以及拓展之自定义组件的事件监听目标DOM元素自定义

1.准备:先封装一个最基本的input组件

按照先前讲到的自定义v-model,我们可以轻松完成一个具备双向绑定的input元素:

<template>
<div>所有作用于当前
  <BaseInput v-model="value"/>
  <br/>
  {{value}}
</div>
</template>
<script>
import BaseInput from "../originEvent/comps/BaseInput.vue"
export default {
  components: {
    BaseInput
  },
  data() {
    return {
      value:'init value'
    }
  }
}
</script>
<template>
    <input name="inputele" :value="value" type="text" @input="$emit('input',$event.target.value)">
</template>
<script>
export default {
  props:['value']
};
</script>

现在它看起来就像这样:

image-20210825215948493

如果对自定义v-model 存在问题,可以参考先这篇文章 《[Vue深入组件]:v-model语法糖与自定义v-model》

2. 利用.native 修饰符,给自定义组件绑定一个focus事件

假如,我们现在需要当鼠标聚焦到输入框时,触发某些操作。我们当然需要绑定一个focus 事件, vue已经为我们提供了@focus 。其用法如:<input @focus/>

既然是封装组件,我们现在需要将这个事件绑定在我们的自定义元素<BaseInput/>上面 ,就像这样:

<template>
  <div>
    <BaseInput v-model="value" @focus="onFocus" />
    <br />
    {{value}}
  </div>
</template>
<script>
import BaseInput from "../originEvent/comps/BaseInput.vue"
export default {
  components: {
    BaseInput
  },
  data() {
    return {
      value: 'init value'
    }
  },
  methods: {
    onFocus() {
      console.log("onfocus trigger!", '--line17');
    }
  }
}
</script>

可以预料到的是,它并不会生效。

这时候,vue为我们解决了这种情况,直接给@focus 加上一个.native修饰符就好了。就像这样。

  <BaseInput v-model="message" @focus.native="onFocus"/>

这样,在你每次鼠标聚焦到输入框,就会触发我们预定义的onFocus method。

image-20210825220948222

至此,如果你想知道.native 究竟是干什么用的,到这里就可以了。

3. 提出问题

不难发现,上面我们自定义的这个组件,实在是太过于简单了,自定义组件<BaseInput/>的根元素,就是一个简单的<input/> 。 但是我们日常工作中,基本上不会去做这种没有多大意义的封装。 如果,这个<input/>元素,并不是自定义组件的根元素,那么.native修饰符,还能生效吗?为了验证想法,我们对自定义组件套上一个label 试一试:

<template>
  <label for="inputele">
    base input:
    <input name="inputele" :value="value" type="text" @input="$emit('input',$event.target.value)">
  </label>
</template>
<script>
export default {
  props: ['value']
};
</script>

父组件依旧不变:

<template>
  <div>
    <BaseInput v-model="value" @focus.native="onFocus" />
    <br />
    {{value}}
  </div>
</template>
<script>
import BaseInput from "../originEvent/comps/BaseInput.vue"
export default {
  components: {
    BaseInput
  },
  data() {
    return {
      value: 'init value'
    }
  },
  methods: {
    onFocus() {
      console.log("onfocus trigger!", '--line17');
    }
  }
}
</script>

此时,会发现,确实失效了。 onFocus method 不会再被触发。

4. 解决问题

实际上,Vue 文档中,也为我们提供了解决方案。 那就是$listeners 这个实例property。

我们先直观的来看一看,这个实例property是什么, 在自定义组件的created() 生命周期函数中,我们尝试打印看看:
image-20210825223729353

可以看到,它收集了所有作用于当前组件的事件监听器

<BaseInput v-model="message" @focus="onFocus" />

你可能不明白这个input从哪里来, 这是因为他被v-model 语法糖覆盖掉了。如果对此存在疑问还是请参考这篇文章link

我们现在知道了有这些事件,但是,我们怎么让这些对应的<input/>元素去监听这些事件呢?

【拓展:】

这里涉及到一个新的知识点,文档中并未发现有单独提及,而是在执行报错时发现的。

我们在之前的学习中,基础部分知道,vue为我们提供了一写常见的DOM监听事件,其中用的最多的就是v-on:click === @click, v-on:change===@change,v-on:focus ===@focus

但是,实际上,v-on 还能够绑定一个监听事件集合(包含一系列监听事件的对象), 即刚才提到的 $listeners 是可以直接被绑定的。

由此, 我们尝试在自定义组件中,显式的指明<input/> 元素,监听$listeners中的所有事件。

<template>
  <label for="inputele">
    base input:
    <input name="inputele" :value="value" type="text" @input="$emit('input',$event.target.value)" v-on="$listeners">
  </label>
</template>
<script>
export default {
  props: ['value']
};
</script>

但是你会发现,现在的情况变得有些怪异:

image-20210826003439345

不仅仅focus 事件没有生效,且原先的双向绑定也失效了。

5. 问题的进一步解决

为什么会出现这个奇怪的问题?

我们再看一下这个自定义组件内部:

    <input name="inputele" :value="value" type="text" @input="$emit('input',$event.target.value)" v-on="$listeners">

可以看到,绑定了一个监听事件@input (即v-on:input),

此外,还绑定了$listeners,而我们刚才说了,它实际上是一个监听事件集合,我们刚才也打印了,它现在里面有两个事件:

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

这也就意味着现在这个<input/> 元素上绑定了三个事件,其中有两个重复的。 应该是这里导致的问题。

为了解决这个问题就需要将监听事件对象做“去重”处理。

有三种方式:

  1. $listeners 中重复的input 监听事件剔除,然后分别绑定只有一个focus事件的$listeners 集合,以及@input事件(也就是保持这部分不变)
  2. $listeners 中的focus 事件单独取出,单独绑定。
  3. 将单独的@input 合并到$listeners 然后只绑定$listeners 这个监听事件集合。

以下方式,父组件都是相同的,且注意需要去除掉之前添加的.native修饰符。即

<template>
  <div>
    <BaseInput v-model="value" @focus="onFocus" />
    <br />
    {{value}}
  </div>
</template>
<script>
import BaseInput from "../originEvent/comps/BaseInput.vue"
export default {
  components: {
    BaseInput
  },
  data() {
    return {
      value: 'init value'
    }
  },
  methods: {
    onFocus() {
      console.log("onfocus trigger!", '--line17');
    }
  }
}
</script>

方式1[不推荐]:“剔除”

这种方式,其实是不推荐的,因为修改了$listeners 这个通用的实例property, 不能保证后期代码的维护。这里为了证明可行性,所以同样尝试实现。

<template>
  <label for="inputele">
    base input:
    <input name="inputele" :value="value" type="text" @input="$emit('input',$event.target.value)" v-on="focusListener">
  </label>
</template>
<script>
export default {
  props: ['value'],
  computed:{
    focusListener:function(){
      delete this.$listeners.input;
      return this.$listeners;
    }
  }
};
</script>

方式2[推荐★]:“单独取出”

这种方式其实和第一种方式是类似的,不同点在于这种方式并不会影响$listeners 在后期的使用。 这种方式可以被使用,但是从html元素上的可读性考虑,不作首要推荐。

<template>
  <label for="inputele">
    base input:
    <input name="inputele" :value="value" type="text" @input="$emit('input',$event.target.value)" v-on="focusListener">
  </label>
</template>
<script>
export default {
  props: ['value'],
  computed: {
    focusListener: function () {
      return { focus: this.$listeners.focus };//绑定到参数的v-on 目标,须是一个Object
    }
  }
};
</script>

方式3[推荐★★★]:“合并”

这种方式也是文档中的实例方式。推荐,因为它并不影响$listeners 在后期的使用。

<template>
  <label for="inputele">
    base input:
    <input name="inputele" :value="value" type="text" v-on="inputListenersObj">
  </label>
</template>
<script>
export default {
  props: ['value'],
  computed: {
    inputListenersObj: function () {
      let _this = this;
      return Object.assign({},_this.$listeners,{
        input:function(e){
          _this.$emit('input',e.target.value)
        }
      })
    }
  }
};
</script>
posted @ 2021-08-26 01:06  暮冬有八  阅读(327)  评论(0编辑  收藏  举报
BACK TO TOP

😀迷海无灯听船行。Github WeChat