前情提要
Vue + element-ui, 一个input框输入订单号,实现按回车或者输入框失去焦点时,向后端发起请求更新数据。
实现方案
定义一个方法searchByOrderId,在输入框上绑两个事件:keyup、blur,都触发searchByOrderId
<template>
<div>
<el-input
v-model="orderId"
placeholder="请输入订单号进行查询"
@keyup.enter.native="searchByOrderId"
@blur="searchByOrderId"
/>
</div>
</template>
<script>
export default {
data () {
return {
orderId: ''
}
},
methods: {
searchByOrderId () {
// 使用 orderId 发请求
if (this.orderId) {
axios.get(this.orderId)
}
}
}
}
</script>
存在的问题
- 输入1后,一直按回车,请求会按一次回车发一次。
- 输入1后,按回车发一次请求,失焦后又会发一次。
总的来说就是orderId没变化时,本该发一次请求就行,却发了多次。
解决方案
多定义一个data数据: oldOrderId,用来保存上一次的orderId。只有两次的值不同时才会重新发请求。
data () {
return {
orderId: '',
oldOrderId: ''
}
},
methods: {
searchByOrderId () {
if (this.orderId && this.oldOrderId !== this.orderId) {
this.oldOrderId = this.orderId // 更新oldOrderId
axios.get(this.orderId)
}
}
}
新的问题
本来想着手动减少请求次数,但后面发现一个BUG,如果用户复制一段orderId去查询,但接口报错(dialog提示),他关闭dialog错误提示后,再次回车重试,却不会发送请求了...(因为orderId没变)
后面实际代码就恢复到开始那样了(或者也可以搞个类似节流一样的),但有同事说属于过度优化,没有必要(一般用户也不会一直回车,恶意人员也不会手动爆接口)
例子的事情到此结束,让我们深入研究下这段代码
我们先看下 element-ui 源码 中 el-input 结构(简化版)
<template>
<div class="el-input">
<input
type="text"
v-bind="$attrs"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
/>
</div>
</template>
<script>
export default {
inheritAttrs: false,
// 函数中都会 $emit 对应的事件
methods: {
handleInput
handleFocus
handleBlur
handleChange
}
}
</script>
我们会发现其实它是一个 div 包裹一个input。而我们项目中在el-input 组件上 双向绑定了orderId,传递了一个placeholder属性。el-input 组件都没有处理,但是可以工作的。
$attrs
可以看到 element-ui 中 el-input 上有一个 v-bind="$attrs"
而且在 script中有个inheritAttrs: false
vue文档对$attrs
的解释 cn.vuejs.org/v2/guide/co…
Vue中有prop特性和非prop特性两种。
一般情况我们写的都是prop特性:父组件向子组件传一些props,子组件里用props来接收并定义它们的类型和默认值等等。
一个非 prop 的特性是指:传向一个组件,但是该组件并没有相应 prop 定义的特性。(通俗的说就是父组件向子组件传了,但子组件的props中并没有定义的那些特性)
正常情况父组件所有的非prop特性都会直接绑定在子组件的根元素上。(prop特性由于子组件props中接收了,所以随意绑定在哪个元素上)
$attrs
即所有非prop特性(除了class和style)。(是一个对象)
// 父组件
<child
class="cls"
style="color: red"
placeholder="请输入订单号进行查询"
val="111"
a="222"
/>
// 子组件
<div class="el-input">
<input
type="text"
/>
</div>
<script>
export default {
props: ['a']
}
</script>
最后子组件渲染为
<div
class="el-input cls"
style="color: red;"
placeholder="请输入订单号进行查询"
val="111"
">
<input type="text" />
</div>
子组件中申明了
porps: ['a']
所以没有 a
所以 porp 特性有:a
, 非 prop特性 有: class, style, placeholder, val
。
$attrs
为: {placeholder: '请输入订单号进行查询', value: '111'}
。
可以看到所有的非 prop 特性都直接绑定在子组件的根元素上。
上面是默认情况
但如果你不希望组件的根元素继非 prop 承特性,你可以在组件的选项中设置inheritAttrs: false
。注意 inheritAttrs: false
选项不会影响 style 和 class 的绑定。
有了inheritAttrs: false 和 $attrs,
你就可以手动决定这些特性会被赋予哪个元素。在撰写基础组件的时候是常会用到的。(参考element-ui 中 el-input)
// 父组件
<child
class="cls"
style="color: red"
placeholder="请输入订单号进行查询"
val="111"
a="222"
/>
// 子组件
<div class="el-input">
<input
type="text"
v-bind="$attrs"
>
</div>
<script>
export default {
inheritAttrs: false,
props: ['a']
}
</script>
此时子组件渲染为
<div
class="el-input cls"
style="color: red;
">
<input
type="text"
placeholder="请输入订单号进行查询"
val="111"
/>
</div>
tips:
obj={a: 'aa', b: 'bb', c: 'cc'}
<div v-bind="obj" ></div>
// 相当于
<div
a="aa"
b="bb"
c="cc"
></div>
现在我们就知道了业务代码中placeholder
为什么可以工作了。(它属于非prop特性,被手动绑定到了 el-input 的 input上面,而不是根元素div上。)
然后研究下给子组件绑定的事件为什么可以执行。
<template>
<div>
<el-input
v-model="orderId"
placeholder="请输入订单号进行查询"
@keyup.enter.native="searchByOrderId"
@blur="searchByOrderId"
/>
</div>
</template>
根据element-ui 源码可以看到 el-input 组件在input上绑定了input focus blur change
事件,并向父组件emit, 因此我们例子中blur是可以正常绑定执行。但没有keyup
事件是没有被emit。 而例子中keyup
事件是用 native
来绑定的。
.native
在一个组件的根元素上直接监听一个原生事件
- 不用
native
时 监听子组件点击事件
// 父组件
<child
@myClick="handleClick"
/>
handleClick () {
console.log('father handleClick')
}
// 子组件
<div @click="handleClick">
child child child child
</div>
handleClick () {
this.$emit('myClick')
}
- 使用
native
时 监听子组件点击事件(效果与上面相同)
// 父组件
<child
@click.native="handleClick"
/>
handleClick () {
console.log('father handleClick')
}
// 子组件
<div>
child child child child
</div>
可以看到使用native
可以 省去子组件 自身绑定click事件,并向父组件$emit 的部分。
使用场景即像上面例子一样。如果子组件click不需要做额外操作,只是向父组件 emit 对应的事件。此时就可以使用native
来简化监听原生事件过程。
在上面element-ui例子中,我们直接在el-input
组件上监听原生keyup事件:<el-input @keyup.enter.native="searchByOrderId" />
。el-input
组件是一个div包裹一个input,按道理我们应该把keyup事件监听到input身上,但这里由于keyup事件会冒泡到div上面,所以没用额外操作。
但有些事件必须手动监听到input上面。$listeners
闪亮登场。
$listeners
为了解决.native
只能在根元素上直接监听一个原生事件。 vue提供了$listeners
。$listeners
也是一个对象,里面包含了作用在这个组件上的(不含 .native 修饰器的)的事件监听器。
你可以手动决定子组件哪个元素 可以触发父组件的事件监听器。
cn.vuejs.org/v2/guide/co…
$listeners
还可以实现隔代组件间通信。
// 父组件
<child1
@myTest1="handleMyTest1"
@myTest2="handleMyTest2"
/>
// 子组件1 child1
<div>
<div>child1 child1</div>
<child2
v-on="$listeners"
/>
</div>
// 子组件1 的子组件 child2. 省去了这个组件 $emit('myTest1') $emit('myTest2')
<div>
<div>child2 child2</div>
<div @click="handleClick">
child222 child222
</div>
</div>
<script>
export default {
methods: {
handleClick () {
this.$emit('myTest1')
this.$emit('myTest2')
}
}
}
</script>
使用$listeners
可以省去中间一层一层的$emit
某个事件。
现在嵌套传递了两层,如果多层并且传递多个事件的话就非常省事了。当然,最好用的还是EventBus和Vuex, 一些特殊情况可以用这个骚操作。