最近在做项目的时候遇到 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}$/)等却无法输入
原因是因为这一类有最少位数, 而我们输入只能一个一个输入, 第一次输入一个, 验证不通过, 返回上一个结果(空值), 如此反复, 永远也无法输入了
对于这一类正则, 目前想到两种方法:
- 不让用户输入, 只能粘贴.
 
- 修改正则表达式, 一位也需要验证通过.
 
两种办法其实都不算好办法, 但目前没有找到合适的解决方案, 等以后找到了再说吧
先把源码奉上