Vue v-for过滤渲染下拉框@change事件参数错位问题排查
问题根因
该问题和bootstrap-vue源码无关,本质是虚拟DOM列表patch过程中的节点位置错位+事件回调闭包引用时序问题,触发链路非常明确:
- 初始状态dropdown1隐藏时,模板filter执行后仅返回
fields[1](dropdown2)1个列表项,虚拟DOM中仅存在1个key为1的<div>节点,节点内的<b-form-select>组件的@change回调闭包绑定的是当前循环上下文的field = fields[1]。 - 操作dropdown2选中「Show dropdown 1」时,
v-model会先同步更新fields[1].value = 1,响应式系统立刻触发组件重渲染:此时filter返回的列表变为[fields[0], fields[1]],顺序上dropdown1排在dropdown2前面。 - Vue执行虚拟DOM对比更新时,会发现新列表比旧列表多了1个头部节点(key=0对应dropdown1),会先在旧的key=1节点前插入新的key=0节点。而
<b-form-select>的change事件触发时机刚好在v-model值更新完成、DOM节点移动完成之前,此时原来绑定了事件的select组件还停留在列表第一个位置,事件触发时读取当前循环上下文的field变量,就会拿到刚插入到第一个位置的fields[0],导致回调参数错位。
你测试把dropdown1放到dropdown2之后不会出问题,就是因为新增的节点在列表尾部,旧的dropdown2对应的节点位置始终是列表第一位,不会出现位置偏移,自然不会出现引用错位。
另外你贴出的showField函数存在逻辑笔误:判断条件field.value !== 'dropdown 1'永远成立(你的field.value存的是0/1数字值,不可能等于字符串dropdown 1),会导致dropdown1始终无法隐藏,实际使用需要改成判断field的id/label来识别目标字段。
规避方案
动态表单类场景下这类联动显隐问题有非常成熟的实现范式,按推荐优先级排序如下:
- 优先遵循动态表单的约定:所有控制其他字段显隐的选择类控件,统一放在被它控制的字段前面,从根源上避免动态新增的字段出现在已有控件的上方,不会触发列表patch时的节点插入错位,改造成本为0。
- 避免在模板内联执行filter逻辑,提前用计算属性处理好可见字段列表:
模板直接遍历计算属性即可:// 脚本中提前计算可见字段,不要在模板渲染时动态过滤 const visibleFields = computed(() => { return fields.filter(field => { if (field.id === 0) return fields[1].value === 1 return true }) })<div v-for="field in visibleFields" :key="field.id"> <b-form-select v-model="field.value" @change="changed(field)"> <option v-for="option in field.options" :key="option.id" :value="option.value"> {{ option.label }} </option> </b-form-select> </div> - 如果必须保留dropdown1在前的顺序,可以改用
v-show控制显隐,不要用v-if/filter销毁/新增节点:注意:不推荐v-for和v-if同层是因为v-if优先级高于v-for,会导致每轮循环重复执行判断且容易出现作用域问题;v-show和v-for同层是合法用法,v-show只是控制元素的display属性,节点始终会被渲染,不会出现新增/销毁节点导致的patch错位。
<div v-for="field in fields" :key="field.id" v-show="showField(field)"> <b-form-select v-model="field.value" @change="changed(field)"> <option v-for="option in field.options" :key="option.id" :value="option.value"> {{ option.label }} </option> </b-form-select> </div>
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

