最近在做项目的时候遇到 input
只能输入整数, 不能输入小数的需求. 想到以前同事封装过这个组件, 可以拿来一用. 结果发现组件只支持正整数, 不支持负数, 看来还需要修改一下了
国际惯例: 源码奉上
同事封装的组件
1 2 3 4 5
| <template> <el-input v-model="model" v-bind="$attrs" @input="_input" v-on="listeners"> <slot v-for="(value, key) in $slots" :name="key" :slot="key"></slot> </el-input> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| <script> export default { inheritAttrs: false, name: 'NumberInput', props: { value: [Number, String], type: { type: String, default: 'decimal' }, places: { type: [Number, String], default: 2 }, Event: { type: String, default: 'input' } }, data() { return { model: this.value } }, watch: { value() { this.model = this.value } }, methods: { _input(val) { const numReg = /^[0-9]$/ let v if (this.type === 'integer') { v = val.replace(/./g, function(e) { if (numReg.test(parseInt(e))) { return e } else { return '' } }) this.$nextTick(() => { this.model = v this.$emit('input', v) }) return } if (this.type === 'decimal') { v = val.replace(/./g, function(e) { if (e === '.' || numReg.test(parseInt(e))) { return e } else { return '' } }) } if (v.indexOf('.') > -1) { let l, r if (v.split('.').length > 2) { v = v.slice(0, v.length - 1) } l = v.split('.')[0] r = v.split('.')[1] || '' if (r.length > +this.places) { r = r.slice(0, +this.places) } v = l + '.' + r } this.$nextTick(() => { this.model = v this.$emit('input', v) }) } }, computed: { listeners() { return Object.assign({}, this.$listeners, { [this.Event]: this._input }) } } } </script>
|
分析组件
首先 html
部分没啥好说的, 都是基本操作, 这里就不再赘述了
js
部分主要看一下 _input
中的代码:
首先定义了一个验证数字的正则, 其实 /^\d+$/
就可以了.
先看验证整数的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| if (this.type === 'integer') { v = val.replace(/./g, function(e) { if (numReg.test(parseInt(e))) { return e } else { return '' } }) this.$nextTick(() => { this.model = v this.$emit('input', v) }) return }
|
这里一切正常, 就是无法判断负整数, 所以我们需要修改一下正则, 并且还需要增加一个参数来判断是正数还是负数
再看验证小数的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| if (this.type === 'decimal') { v = val.replace(/./g, function(e) { if (e === '.' || numReg.test(parseInt(e))) { return e } else { return '' } }) }
if (v.indexOf('.') > -1) { let l, r if (v.split('.').length > 2) { v = v.slice(0, v.length - 1) } l = v.split('.')[0] r = v.split('.')[1] || '' if (r.length > +this.places) { r = r.slice(0, +this.places) } v = l + '.' + r } this.$nextTick(() => { this.model = v this.$emit('input', v) })
|
小数验证一下就出来这么多问题了:
- 小数点后没有数字也会验证通过
- 在第一个小数点前面再输入一个小数点会导致数据丢失
关于数据丢失的例子:
输入 |
处理 |
输出 |
123.45 |
验证通过 |
123.45 |
12.3.45 |
进入阻止输入两个小数点的逻辑, 截断最后一个字符 5 |
12.3.4 |
|
继续进入截取小数点的逻辑, 只取数组前两位, 并验证小数位数是否超过预定值 |
12.3 |
改造组件
增加一个参数, 来判断正负数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| props: { type: { type: String, default: 'decimal' }, signed: { type: String, default: 'plus' }, places: { type: [Number, String], default: 2 } }, computed: { regs() { return { 'integer-plus': /^\d+$/, 'integer-minus': /^-\d*$/, 'integer-all': /^-?\d*$/, 'decimal-plus': new RegExp(`^\\d+(\\.\\d{0,${this.places}})?$`), 'decimal-minus': new RegExp(`^-(\\d*|\\d+(\\.\\d{0,${this.places}})?)$`), 'decimal-all': new RegExp(`^-?(\\d*|\\d+(\\.\\d{0,${this.places}})?)$`) } }, regName() { return `${this.type}-${this.signed}` }, reg() { return this.regs[this.regName] } }
|
验证输入
思路: 感觉同事验证整数和小数的逻辑太麻烦, 其实可以根据不同情况生成不同正则, 如果验证通过就返回, 否则就截断最后一个字符再返回. 但这样也犯了同事处理阻止输入两个小数点的问题: 如果在字符串中间输入非法字符, 会截断最后一个合法字符, 而留下这个非法字符.
其实, 如果本次输入验证不通过, 那我就返回上一次输入的. 这就需要记录本次输入和上次输入的值, 需要请出 watch
大神了, 并且需要一个变量记录当前的输入值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| data() { return { model: this.value, temporary: this.value } }, watch: { value(n) { this.temporary = n }, temporary(n, o) { if (n.length && !this.reg.test(n)) { n = o this.temporary = o return } this.model = n this.$emit('input', n) } }, methods: { _input(val) { this.temporary = val } }
|
至此就算正式改造完成了
问题
-
或 .
问题(未解决)
可以输入 -
的时候, 只输入一个 -
也可以验证通过
可以输入 .
的时候, .
后面没有数字也可以验证通过
这两种情况暂时没有好的解决方案.
初始值为不合法字符串(已解决)
如果初始值就是一个不合法的字符串, 会进入无限循环中
可以在 created
对 value
判断一下, 如果不合法就给 temporary
和 model
赋值为空.
1 2 3 4 5
| if (!this.reg.test(this.value)) { this.model = '' this.temporary = '' }
|
这种虽然会改变输入值, 但你传入的就是一个非法值, 总比陷入无限循环而卡死页面强吧
默认值为 undefined 会造成页面卡顿甚至卡死(已解决)
当在表格中大量使用该组件并且默认值为 undefined
时, 渲染需要很长时间, 甚至卡死浏览器
如图:
这个表格默认查最近 4
天的数据, 由于是动态表格, 所以由后端返回, prop
是当日的时间戳.
表头数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| data: [ { children: [ { prop: '1588953600000', label: '09日', }, { prop: '1589040000000', label: '10日', }, { prop: '1589126400000', label: '11日', }, { prop: '1589212800000', label: '12日', }, ], prop: '1589262808249', label: '2020年05月', } ]
|
表格数据:
后端目前的做法是当日如果有数据就返回当日 key
和 value
, 否则就没有当日这个字段
1 2 3 4 5 6 7 8 9 10 11
| data: [ { id: '1611934516379702', '1588953600000': 10, name: '11' }, { id: '1611934516379703', name: '22' }, ]
|
第一条 09
日有数据, 就返回了, 其余日都没数据, 就没有返回; 第二条都没有数据, 所以都没有返回
这样取 10 - 12
日的数据就是 undefined
, 当页面中有大约 150
个该组件并且都是 undefined
的时候, 渲染就非常卡顿了, 数据量再大就会卡死页面了
为什么发现是 undefined
引起的呢? 因为还有一个类似的页面并不卡顿, 差别就在那个页面后端默认返回的是 0
, 而不是 undefined
.
一开始想的是让后端也默认返回 0
, 但以后出现类似情况都需要后端处理, 作为一个成熟的组件, 咱要从自身解决这个问题
说说我的看法: 当前的问题是 undefined
引起的, 为什么会这样呢? 其实是因为咱们接收 value
值规定了只接收 [Number, String]
, 但传入的是 undefined
, 但 vue
却没有报类型错误, 应该是内部对这种情况做了处理, 导致占用了大量资源, 导致渲染卡顿. 所以咱们只要给一个类型是 Number
或 String
的默认值, 问题就解决了
1 2 3 4 5 6
| props: { value: { type: [Number, String], default: '' } }
|
传入的 value 为 Number 类型时无法验证(已解决)
复现路径: 不通过输入框修改 value
, 而是通过代码将 value
值改变为 Number
类型
以前都是通过输入框来验证组件是否可以控制输入, 这种方式会将 Number
转化为 String
, 导致一直没有测出这个 bug
由于 Number
没有 length
属性, 并且 RegExp
只能验证字符串(验证数值有问题), 所以 n.length && !this.reg.test(n)
这个判断就进不去了, 从而导致无法验证
所以我们需要先转化为 String
再验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| watch: { temporary(n, o) { n = n + '' if (n && !this.reg.test(n)) { n = o this.temporary = o return } this.model = n this.$emit('input', n) } }, created() { const val = this.value + '' if (val && !this.reg.test(val)) { this.temporary = '' } }
|
扩展
既然现在是通过传入参数来生成正则表达式, 那何不封装一个接收正则表达式的组件, 这样不就可以让使用者自定义 input
值了吗?
RegInput.vue
1 2 3 4 5
| <template> <el-input v-model="model" v-bind="$attrs" @input="_input" v-on="$listeners"> <slot v-for="(value, key) in $slots" :name="key" :slot="key"></slot> </el-input> </template>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <script> export default { inheritAttrs: false, name: 'RegInput', props: { value: { type: [Number, String], default: '' }, reg: { type: RegExp, default: /./ } }, data() { return { model: this.value, temporary: this.value } }, watch: { value(n) { this.temporary = n }, temporary(n, o) { n = n + '' if (this.test(n)) { n = o this.temporary = o return } this.model = n this.$emit('input', n) } }, methods: { _input(val) { this.temporary = val }, test(val) { return val && !this.reg.test(val) } }, created() { const val = this.value + '' if (this.test(val)) { this.temporary = '' } } } </script>
|
验证发现中文(/^[\u4E00-\u9FA5]*$/
)和数字(/^-?(\d*|\d+(\.\d*)?)$/
)可以正常输入, 但手机号(/^1[3456789]\d{9}$/
)等却无法输入
原因是因为这一类有最少位数, 而我们输入只能一个一个输入, 第一次输入一个, 验证不通过, 返回上一个结果(空值), 如此反复, 永远也无法输入了
对于这一类正则, 目前想到两种方法:
- 不让用户输入, 只能粘贴.
- 修改正则表达式, 一位也需要验证通过.
两种办法其实都不算好办法, 但目前没有找到合适的解决方案, 等以后找到了再说吧
先把源码奉上