Element UI el-cascader 显示对、校验红:整对象替换漏写 key,undefined 中间态卡死校验
某后台管理系统的"编辑"抽屉复用了"新增"表单组件。产品验收时发现一个诡异现象:打开任意一条记录的编辑抽屉,省市区级联选择器已经正确显示了"江苏省 / 盐城市 / 亭湖区",但 el-form-item 的边框是红色的,下方还带着红字"请选择省市区"——用户什么都没碰,表单就处于"显示对、校验红"的矛盾状态。
同事加了一行 this.$refs.form.clearValidate() 之后当场消失,以为修好了。结果第二天测试又偶发复现,在异步加载省市区树比较慢的时候更容易出现。
现象
- 打开编辑弹窗,cascader 显示正确文本,el-form-item 红框 + 红字
- 用户未操作任何字段,表单已处于校验失败态
- 仅加
clearValidate()能临时压住,异步竞争时仍会复现 - 新增弹窗(留空提交)校验行为完全正常,只有编辑回显时出现
根因
问题的触发路径需要同时满足三个条件:
- 表单
data()中 cascader v-model 字段初始值是数组(selectedOptions: []) - el-form-item 配置
prop+required规则(trigger 默认包含change) - 打开编辑时用整对象替换回显表单,且新对象字面量漏写了 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.vue 的 checkedValue 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],两者相等,!isEqual 为 false;且 value 不是 undefined,所以整个 if 判断为 false,不再 dispatch change 事件,form-item 的错误状态从此卡死。
而 cascader 显示的文本是通过 presentText(基于 value + options 派生)单独渲染的,只要 value 数组和 options 就位,文字就是对的——这就形成了"显示正确、校验永红"的矛盾假象。
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填入正确值后,在$nextTick里clearValidate(['xxx']),覆盖异步竞争(options 后到、接口慢)等边缘路径
- 修复点只需加一行:在
this.form = {...}里补上selectedOptions: [] $nextTick里再clearValidate是兜底,不是主修- 同样适用于所有 array/object 型 v-model:
el-select multiple、el-checkbox-group、el-date-picker type="daterange"、自定义文件列表
有人会想用 Object.assign(this.form, data) 来替代整对象替换,认为这样不会删除已有 key。但这只是凑巧能规避问题——初始化阶段如果 data() 里漏写了,同样会在赋值后产生 undefined 中间态。治本的做法是:data() 里对所有 array 型字段明确初始化,然后不论用哪种赋值方式都要在替换对象里显式列出。
排查备忘
遇到"显示对、校验红"且 clearValidate() 可临时压住的 el-form-item,优先检查:
- 打开编辑时是否有整对象替换(
this.form = {...}) - 新对象里是否缺少了 array/object 类型的 v-model key
- 补全 key 后
$nextTick → clearValidate双保险
不要反复加 clearValidate() 兜——那是在盖住症状,根因没动。
一句话外卖
整对象替换回显表单时,漏写任何一个 array 型 v-model key,该字段就会经历
[] → undefined → [a,b,c]三态;el-cascader 的内部 watch 守卫会阻止"已正确值"再次 dispatch change,校验脏污从此卡死——根治靠显式列全所有 key,不靠 clearValidate。