最近在用 Element Plus 的 <ElInput> 组件做一个限制最大值 + 格式化显示的需求时,踩了一个特别隐蔽的坑:
输入框显示被限制成了 500,但实际绑定的数据还是 1000
看似小问题,其实背后涉及到 formatter / parser 的本质区别、Element Plus 的内部设计逻辑,以及 Vue 双向绑定的行为模型。

1. 需求背景

我有一个百分比输入框:

  • 最大值不能超过 500(例如业务里代表最大 500%)
  • 需要用 formatter 美化显示
  • 双向绑定到表格里的某一列数据

代码如下:

<ElInput
  type="number"
  v-model={row[TableDatePresetsEnum[dateLabel]]}
  placeholder="请输入"
  formatter={moneyFormatter}
  v-slots={{
    append: () => '%',
  }}
/>

问题出现了:

输入 1000,组件显示是 500(moneyFormatter 生效),但绑定的数据却是 1000!

2. 第一个误区:formatter ≠ 数据限制器

很多人直觉会认为:

formatter 会改变 v-model 的值

但实际上,Element Plus 官方文档也明确说了

formatter 用于输入框显示格式化,只作用于 UI 层,不会影响真实的输入值。

换句话说:

  • formatter = 美化输出

  • parser = 解析输入

2.1 为什么 formatter 不改数据?

这是组件设计的哲学问题。formatter 的职责是单向输出格式化,它要做到的是:

  1. 保留用户的原始输入(可能是还没完成的输入)

  2. 在视觉上提供美化(千分位、货币符号、掩码等)

如果 formatter 同时去改数据,就会出现以下问题:

  • 用户输入 "12."(还没输入小数位),formatter 改成 "12" → 小数点被吞掉

  • 用户输入 "0005",formatter 改成 "5" → 原始意图丢失

这种情况在用户体验上是灾难性的,所以 Element Plus 选择让 formatter 只改显示,不动真实值

3. 真实数据的修改要靠 parser

Element Plus 给了另一个属性 parser,用来做输入解析:

<ElInput
  v-model={value}
  formatter={(val) => `${val}%`} // 显示加个百分号
  parser={(val) => {
    const num = Number(val.replace('%', ''));
    return num > 500 ? 500 : num; // 限制最大值
  }}
/>

这样:输入 1000,parser 改为 500 写回 v-model,formatter 将 500 显示为 500%。

这才是官方推荐的方式。

或者使用 onInput 事件手动更改:

<ElInput
  type="number"
  modelValue={value}
  onInput={(val) => {
    const num = Number(val);
    value = num > 500 ? 500 : num;
  }}
  formatter={(val) => `${val}%`} // 显示加个百分号
  v-slots={{
    append: () => '%',
  }}
/>

4. Element Plus 源码

在 packages/components/input/src/input.vue 里,formatter 的调用是在渲染阶段

const displayValue = computed(() => {
  let formatted = inputValue.value
  if (props.formatter) {
    formatted = props.formatter(formatted)
  }
  return formatted
})

而 v-model 的值是通过事件 onInput 直接回传的,没有经过 formatter 的处理:

const handleInput = (event: Event) => {
  const { value } = event.target as HTMLInputElement
  emit('update:modelValue', value)
}

这就解释了为什么 UI 和数据会“脱节”。

4.1 延伸:其他框架的类似设计

这个 formatter / parser 分离的思想,不止 Element Plus 用:

  • Ant Design (InputNumber) 同样区分 formatter / parser

  • React Final Form 也是 format / parse 分离

这是表单组件的通用模式:

  • parse:将用户输入的字符串解析成数据模型

  • format:将数据模型转换成显示字符串

总结:这个问题表面上只是一个“为什么显示和数据不一致”的 Bug,但背后涉及:

  • Vue v-model 的数据流

  • UI 层与数据层解耦的设计哲学

  • 表单组件 formatter / parser 的职责分离

  • 用户体验与数据安全性的平衡

当理解了这些就会明白:

任何一个 UI 层的 API,只要它不承诺会改数据,那它就不应该被用来改数据。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐