返回博客

Element UI el-cascader 显示对、校验红:整对象替换漏写 key,undefined 中间态卡死校验

某后台管理系统的"编辑"抽屉复用了"新增"表单组件。产品验收时发现一个诡异现象:打开任意一条记录的编辑抽屉,省市区级联选择器已经正确显示了"江苏省 / 盐城市 / 亭湖区",但 el-form-item 的边框是红色的,下方还带着红字"请选择省市区"——用户什么都没碰,表单就处于"显示对、校验红"的矛盾状态。

同事加了一行 this.$refs.form.clearValidate() 之后当场消失,以为修好了。结果第二天测试又偶发复现,在异步加载省市区树比较慢的时候更容易出现。

现象

  • 打开编辑弹窗,cascader 显示正确文本,el-form-item 红框 + 红字
  • 用户未操作任何字段,表单已处于校验失败态
  • 仅加 clearValidate() 能临时压住,异步竞争时仍会复现
  • 新增弹窗(留空提交)校验行为完全正常,只有编辑回显时出现

根因

问题的触发路径需要同时满足三个条件:

  1. 表单 data() 中 cascader v-model 字段初始值是数组(selectedOptions: []
  2. el-form-item 配置 prop + required 规则(trigger 默认包含 change
  3. 打开编辑时用整对象替换回显表单,且新对象字面量漏写了 cascader 的 key
async getDetail(id) {
  const { data } = await api.detail(id)
  this.form = {
    id: data.id,
    name: data.name,
    address: data.address,
    // ← selectedOptions 没有写!
  }
  await this.loadAreaTree()
  this.$set(this.form, 'selectedOptions', this.toCodes(data))  // 之后才补上
}

实际数据流分三步:

| 步骤 | form.selectedOptions | cascader 内部 | 校验结果 | |------|------------------------|--------------|---------| | 初始 | [] | 正常 | 通过 | | this.form = {...} 不含该 key | undefined | value watch 触发,dispatch el.form.change [undefined] | required 失败,红框红字 | | $set('selectedOptions', [a,b,c]) | [a,b,c] | checkedValue watch:!isEqual([a,b,c],[a,b,c]) ‖ isUndefined([a,b,c]) = false不 dispatch | 错误状态滞留 |

关键在 cascader.vuecheckedValue watch 守卫(Element UI 2.12.0,L313-328):

watch: {
  checkedValue(val) {
    if (!isEqual(val, this.value) || isUndefined(this.value)) {
      this.$emit('input', val)
      this.dispatch('ElFormItem', 'el.form.change', [val])
    }
  }
}

$set 把正确值填回来时,checkedValue 已经同步更新为 [a,b,c]this.value(父组件 v-model)也已是 [a,b,c],两者相等,!isEqualfalse;且 value 不是 undefined,所以整个 if 判断为 false不再 dispatch change 事件,form-item 的错误状态从此卡死。

而 cascader 显示的文本是通过 presentText(基于 value + options 派生)单独渲染的,只要 value 数组和 options 就位,文字就是对的——这就形成了"显示正确、校验永红"的矛盾假象。

坑:整对象替换时漏写数组型 v-model key

this.form = {...} 只要不显式列出某个 key,该 key 就从对象上消失,Vue 访问时得到 undefined。对于 el-cascader 这类 array-typed v-model,undefined 会立刻触发一次 required 失败的 change 事件。而 cascader 内部 !isEqual ‖ isUndefined 守卫保证了"值回来之后不重复 dispatch",结果校验脏污就永久卡在那里。

仅靠 clearValidate() 无法根治,它只清 validateState/validateMessage,不补 dispatch。

正解

根因修:整对象替换时显式列出所有 array/object 类型的 v-model 字段,杜绝 undefined 中间态。

 async getDetail(id) {
   const { data } = await api.detail(id)
   this.form = {
     id: data.id,
     name: data.name,
     address: data.address,
+    selectedOptions: [],   // 显式列出,初始给 [],永不经过 undefined
+    images: [],            // 同理,所有数组型 v-model 都要写
+    tags: [],
   }
   await this.loadAreaTree()
   this.$set(this.form, 'selectedOptions', this.toCodes(data))
+  this.$nextTick(() => {
+    this.$refs.form && this.$refs.form.clearValidate(['selectedOptions'])
+  })
 }

两层防御:

  • 层 1(必做):整对象替换时列出全部 array-typed v-model 字段,初始值 [],绝不让字段经过 undefined 中间态
  • 层 2(兜底)$set 填入正确值后,在 $nextTickclearValidate(['xxx']),覆盖异步竞争(options 后到、接口慢)等边缘路径
正解:整对象替换时显式列全所有 array 型 key
  • 修复点只需加一行:在 this.form = {...} 里补上 selectedOptions: []
  • $nextTick 里再 clearValidate 是兜底,不是主修
  • 同样适用于所有 array/object 型 v-model:el-select multipleel-checkbox-groupel-date-picker type="daterange"、自定义文件列表
为什么不用 Object.assign?

有人会想用 Object.assign(this.form, data) 来替代整对象替换,认为这样不会删除已有 key。但这只是凑巧能规避问题——初始化阶段如果 data() 里漏写了,同样会在赋值后产生 undefined 中间态。治本的做法是:data() 里对所有 array 型字段明确初始化,然后不论用哪种赋值方式都要在替换对象里显式列出。

排查备忘

遇到"显示对、校验红"且 clearValidate() 可临时压住的 el-form-item,优先检查:

  1. 打开编辑时是否有整对象替换(this.form = {...}
  2. 新对象里是否缺少了 array/object 类型的 v-model key
  3. 补全 key 后 $nextTick → clearValidate 双保险

不要反复加 clearValidate() 兜——那是在盖住症状,根因没动。

一句话外卖

整对象替换回显表单时,漏写任何一个 array 型 v-model key,该字段就会经历 [] → undefined → [a,b,c] 三态;el-cascader 的内部 watch 守卫会阻止"已正确值"再次 dispatch change,校验脏污从此卡死——根治靠显式列全所有 key,不靠 clearValidate。