手动实现一个输入限制指令

3 分钟阅读

手动实现一个输入限制指令

"输入框只能输入数字"是表单开发里的高频需求。最常见的做法是在 oninput 里用正则过滤非法字符,但这个方案在中文输入法下会翻车。本文分析问题产生的原因,并用一个 Vue 自定义指令给出可靠的实现。

1. 问题:oninput 方案在输入法下失效

最初的实现很朴素:监听 input 事件,把非数字字符替换掉。但在"只能输入数字"的输入框里用中文输入法打字时,出现了一个诡异的现象:输入框里显示的内容是对的,v-model 绑定的值却再也不更新了——之后不管输入什么,绑定的数据都拿不到正确的值。

原因在于我们手动修改 el.value 是"绕过事件系统"的:

  1. 输入法组合(composition)期间,浏览器对 input 事件的触发有自己的节奏
  2. 我们在 oninput 里直接改写 el.value,这个赋值操作不会触发新的 input 事件
  3. Vue 的 v-model 依赖 input 事件来同步数据,事件没有派发,数据层和视图层就此脱节

所以正确的做法是:过滤完值之后,手动把 input 事件补发出去,让 v-model 的同步机制照常工作。

2. 指令实现

把过滤逻辑封装成一个自定义指令,并通过修饰符支持浮点数场景:

javascript
function onInput(el, ele, binding, vnode) {
  // 用闭包实现"只保留第一个小数点":
  // 每次调用 flag 自增,只有第一次返回 '.',之后都返回空串
  function regHandle() {
    let flag = 0
    return () => (flag++ === 0) ? '.' : ''
  }

  function handle() {
    let val = ele.value

    // modifiers 为修饰符对象,传入 .float 则按浮点数规则处理
    if (binding.modifiers.float) {
      // 只保留数字和小数点
      val = val.replace(/[^\d.]/g, '')
      // 不允许出现多个小数点,只保留第一个
      val = val.replace(/\./g, regHandle())
      // 不允许以小数点开头,自动补 0
      val = val.replace(/^\./g, '0.')

      if (binding.value) {
        // 指令的值用于指定期望保留的小数位数,只允许正整数
        let points = +binding.value
        if (!isNaN(points)) {
          if (!Number.isInteger(points) || points < 0) {
            points = 0
          }
          const reg = new RegExp(`^(\\d+)\\.(\\d\{${points}}).*$`)
          if (points === 0) {
            // 保留 0 位小数:小数点及之后的内容全部去掉
            val = val.replace(reg, '$1')
          } else {
            // 通过正则捕获保留小数点后指定位数
            val = val.replace(reg, '$1.$2')
          }
        }
      }
    } else {
      // 整数场景:只保留数字
      val = ele.value.replace(/[^\d]/g, '')
    }

    ele.value = val

    // 关键步骤:手动派发 input 事件,保证 v-model 的响应式同步
    // 组件内 input 和原生 input 的事件机制不同,需要区分处理
    if (vnode.componentInstance) {
      vnode.componentInstance.$emit('input', ele.value)
    } else {
      vnode.elm.dispatchEvent(new CustomEvent('input', ele.value))
    }
  }
  return handle
}

const numberLimit = {
  bind(el, binding, vnode) {
    // 指令可能直接挂在 input 上,也可能挂在包裹了 input 的组件上
    const ele = el.tagName === 'INPUT' ? el : el.querySelector('input')
    ele.addEventListener('input', onInput(el, ele, binding, vnode), false)
  }
}

3. 使用方式

html
<!-- 只能输入整数 -->
<input v-model="age" v-number-limit>

<!-- 只能输入数字,最多两位小数 -->
<input v-model="price" v-number-limit.float="2">

4. 实现要点回顾

  • 指令挂点的兼容:bind 里先判断 el.tagName,兼容直接挂在 <input> 和挂在封装组件上两种用法
  • 闭包保留第一个小数点:regHandle 每次 replace 都返回一个全新的计数函数,利用 String.replace 对每次匹配调用回调的特性,实现"只留第一个"
  • 手动补发事件是核心:没有这一步,v-model 就会和视图脱节——这也是整个方案区别于普通 oninput 过滤的关键

在这个基础上还可以扩展更多规则,例如最大值/最小值限制、只允许正数/负数、手机号分段格式化等,思路都是一致的:过滤值 → 写回视图 → 补发事件同步数据。

相关文章
前后篇
← NEWERVue3+webpack实现组件远程加载OLDER →聊一聊京东的面试(金融组&物流组)
评论