问题
问题源自需求中的一个 bug:
项目使用 iview,需求是一个多行输入框,仅允许输入数字,可以换行,使用 input 组件:
<Input
v-model="form.ids"
type="textarea"
:number="true"
@on-change="handleChange"
/>
handleChangeLimit(event) {
const value = event.target.value;
if (value) {
this.form.ids = value.replace(/[^\d\n]/g, "");
}
},
一开始是这样的,在换行时确实能处理过滤非数字内容。但是在输入的过程中、以及 blur 时不会处理数据。例如:
nextTick
有点不可思议,首先 on-change 是在输入框“改变”的时候进行事件处理,为什么输入的过程中没有变化?
看起来只是 Vue 简单的数据绑定,为什么没有生效呢?
在请教同事之后,加上 nextTick 就好了:
<Input
v-model="form.ids"
type="textarea"
:number="true"
@on-change="handleChange"
/>
handleChangeLimit(event) {
const value = event.target.value;
if (value) {
// 注意此处 nextTick
this.$nextTick(() => {
this.form.ids = value.replace(/[^\d\n]/g, "");
});
}
},
这是什么原因呢?虽然知道 nextTick 的作用,但是还是不理解,此处为什么需要 nextTick 呢?
Vue 官网深入响应式原理一节关于 nextTick 的介绍:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
例如,当你设置
vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用
按说我们不需要关注执行的循环队列,只是改变输入框的值而已啊,这也正是上面所说的。
此段可放在最后再看
实际上确实是这样(后面还会说到),问题在于这个需求的特殊,在处理数据的过程中,如果输入非数字,就会转换为空,也就是最终的结果和之前的结果是一样的,导致 Vue 不再渲染页面,所以看起来数据(this.form.ids)更新了,页面没有变化。
为了说明其中一些原理,我做了一些简单的尝试:
v-model 双向数据绑定
<script>
export default {
data() {
return {
value: "",
};
},
};
</script>
<template>
<h1>{{ value }}</h1>
<input v-model="value" />
</template>
再正常不过的效果
v-model 语法糖的本质
<script>
export default {
data() {
return {
value: "",
};
},
methods: {
handleInput(event) {
const value = event.target.value;
this.value = value;
},
}
};
</script>
<template>
<h1>{{ value }}</h1>
<input :value="value" @input="handleInput" />
</template>
这样也很好理解。
输入框过滤非数字
现在加上我们的需求,限制输入为数字。
<script>
export default {
data() {
return {
value: "",
};
},
methods: {
handleInput(event) {
const value = event.target.value;
this.value =value.replace(/[^0-9\.]+/g, '');
},
}
};
</script>
<template>
<h1>{{ value }}</h1>
<input :value="value" @input="handleInput" />
</template>
有问题,改变 this.value 页面并没有跟着渲染。为什么双向绑定的值没有渲染页面?
输入框过滤非数字,使用 nextTick
还是加上 nextTick:
<script>
export default {
data() {
return {
value: "",
};
},
methods: {
handleInput(event) {
const value = event.target.value;
this.$nextTick(() => {
this.value =value.replace(/[^0-9\.]+/g, '');
})
},
}
};
</script>
<template>
<h1>{{ value }}</h1>
<input :value="value" @input="handleInput" />
</template>
然而,却不生效了?开头我们解决问题的方法,不就是用 nextTick 吗?此时怎么不行了呢?
输入框过滤非数字,注意双向绑定
<script>
export default {
data() {
return {
value: "",
};
},
methods: {
handleInput(event) {
const value = event.target.value;
// 注意此处
this.value = value;
this.$nextTick(() => {
this.value = value.replace(/[^0-9\.]+/g, '');
})
},
}
};
</script>
<template>
<h1>{{ value }}</h1>
<input :value="value" @input="handleInput" />
</template>
因为我们还没有进行双向绑定,这样在 nextTick 时,this.value 的值没有变化,没有变化就不会渲染页面。所以加上 this.value = value; 就好了。就是要先完成双向绑定,此时输入框的输入内容就会双向绑定到 this.value,比如 66a,nextTick 的作用是,在下一个事件循环中,再对 this.value 赋值,此时为 66,这样值不一样,就触发了渲染。这就是为什么需要 nextTick。
输入框过滤非数字,使用 v-model 的双向绑定
<script>
export default {
data() {
return {
value: "",
};
},
methods: {
handleChange(e) {
const value = e.target.value;
console.log("已经绑定 value:", this.value);
this.value = value.replace(/[^0-9\.]+/g, "");
console.log("处理后的 value:", this.value);
},
},
};
</script>
<template>
<h1>{{ value }}</h1>
<input v-model="value" @input="handleChange" />
</template>
为什么使用 v-model,就不需要 nextTick 了呢?
因为 v-model 已经完成了双向数据绑定,我们单独写了 input 事件处理需求,相当于此处有两个 input 事件,自己写的 input 事件执行时,v-model 的 input 事件已经绑定了值。
iview on-change 事件
回到最开始 bug 的解决,只是加了 nextTick,并没有赋值啊?
这个在 iview 源码中有解答:github.com/iview/iview…
<input
:id="elementId"
:autocomplete="autocomplete"
:spellcheck="spellcheck"
ref="input"
:type="type"
:class="inputClasses"
:placeholder="placeholder"
:disabled="disabled"
:maxlength="maxlength"
:readonly="readonly"
:name="name"
:value="currentValue"
:number="number"
:autofocus="autofocus"
@keyup.enter="handleEnter"
@keyup="handleKeyup"
@keypress="handleKeypress"
@keydown="handleKeydown"
@focus="handleFocus"
@blur="handleBlur"
@compositionstart="handleComposition"
@compositionupdate="handleComposition"
@compositionend="handleComposition"
@input="handleInput"
@change="handleChange">
handleInput (event) {
if (this.isOnComposition) return;
let value = event.target.value;
if (this.number && value !== '') value = Number.isNaN(Number(value)) ? value : Number(value);
this.$emit('input', value);
// 2)
this.setCurrentValue(value);
// 1)
this.$emit('on-change', event);
},
handleChange (event) {
this.$emit('on-input-change', event);
},
setCurrentValue (value) {
if (value === this.currentValue) return;
this.$nextTick(() => {
this.resizeTextarea();
});
this.currentValue = value;
if (!findComponentUpward(this, ['DatePicker', 'TimePicker', 'Cascader', 'Search'])) {
this.dispatch('FormItem', 'on-form-change', value);
}
},
- 1): 原来
on-change事件实际上封装的是input事件 - 2): 在
input事件中处理了双向数据绑定的问题
总结
- v-model 绑定 value 和 input 事件
- Vue DOM 异步更新,数据没有更新,wather 不会调用
- nextTick 让我们对数据的处理在下一个事件循环生效,避免了 wather 被去重,只执行一次。