线上问题
本周五下午的时候,正在排查一个打包后 2 行样式代码变 1 行的问题,突然被拉进一个新群,有人反馈说是提交表单的一个 id 字段与预期不符,而后端排查到在页面上那个 id 是不可更改的,怀疑前端传参错误。我心里暗暗一惊:又来 bug 了!
一开始真没看懂哪里会改错那个参数,毕竟在那个弹框文件内,只能搜到一行代码会对那个id进行赋值,该代码写在 watch 中,监听的是一个 prop,该回调如果触发,外部对象必然已经发生改变。
不过后面自己还是复现了,并且写了个demo,大家可以直接访问这个链接复现问题。

复现步骤
- 刚打开页面的时候,App.vue中
injectData.id为 1,然后打开一次弹框
此时传进去的是 1,弹框内监听了传进来的对象,且立即执行,所以表单展示也是 1,正常
- 随便填写内容(可不填),然后关闭弹框
此时执行关闭弹框的回调,回调内调用了表单的
resetFields方法(这里就是一开始排查没有注意到的地方)
- 点击修改 id 的按钮
此时必然触发弹框内的 watch 回调
- 再次打开弹框
此时传进去的是 2(watch 回调函数中更新),且content字段是空的(第 2 步中表单重置),正常
- 随便填写内容(可不填),然后关闭弹框
此时再次执行关闭弹框的回调,表单再次重置
- 不要修改 id,直接再次打开弹框
此时 id 为 1,不正常
排查源码
根据上述步骤,就开始怀疑表单的resetFields方法的问题了,于是看源码:

发现会遍历this.fields,调用内部的resetField方法。于是又想着:
this.fields是什么?resetField方法是如何实现的?
第一个问题的答案还是很好找的,就在同一个文件内:

原来this.fields的每一项都是在el.form.addField事件触发时新增的,于是全局搜索el.form.addField,很好找,只有一个地方会触发:

原来是表单项 mounted 的时候会触发这个事件,并且参数就是这个表单项,也就是说this.fields的每一项,都是表单项。
那么就继续查表单项内的resetField方法:

原来是每个表单项都会保存this.initialValue,重置的时候就重新更新为该值。
那么this.initialValue从哪里来?
其实还是上面那个表单项的mounted:

好了,现在可以知道:
当我关闭弹框的时候,会调用表单组件的resetFields方法,该方法会把表单项的值重置为表单项mounted的时候的值。
那么表单项mounted的时候,是什么值?
看表现其实可以看出来,就是第一次打开弹框时的值。刚打开页面的时候,injectData.id 是 1,如果一开始先修改injectData.id为 2,再按照上述步骤复现,会发现最后的injectData.id是 2.
反正都到这一步了,还是继续看el-dialog的源码吧:

弹框内的主体部分,有个 v-if,太熟悉了,如果变量为 false,是根本不会渲染的。那么这个值到底是不是false呢?
文件内查找,只能查找到 2 处地方。

一处是上面那里,另一处就是mounted中:

一开始刷新页面的时候,this.visible是false的,所以这个 if 进不去,this.rendered肯定是undefined,也就不会渲染了,但是 this.visible改变的时候也没看到会重新设置这个值呀,好神奇,然后我打了好多断点,才发现是el-dialog组件通过 minxin 的形式引入了el-popup,这里会调用open方法:

就是这个open方法,会修改rendered,于是就渲染了:

修复 BUG
挺累的,查了半天又写了半天……
一开始我觉得是表单组件的resetFields方法的问题,要是该方法能重置成我刚开始传入的数据就好了,不过试了下,即使业务组件中的data里是空值,由于watch里设置了immediate,也还是会在表单项组件mounted之前执行的,所以表单项拿到的数据一开始就是有值的。而如果不设置immediate,那么第一次打开必然会异常,这种情况当初开发的时候可以测出来,但是最后可能还是通过加上immediate的方式进行解决……
修复方式有:
- 把监听
propData改为监听dialogVisible,每次打开弹框时均初始化数据,关闭时无需做任何操作
目前比较倾向的方案
- 关闭时不调用
form组件的内置resetFields函数,而是手动有选择地重置部分字段
总觉得有点繁琐
- 不使用
watch,在模板和提交时都手动合并form和propData的数据
但是我又觉得这样比较分离,总是想合起来
- 父组件不传
prop,子组件不监听,父组件打开弹框时直接调用子组件的方法,把参数传进去初始化
从一些开源库看到的,但是感觉调私有方法好像不是太好,而且也是打开弹框时都会执行,执行频率与方法1相同
- 设置弹框关闭时即销毁
destroy-on-close,每次打开弹框都重新执行一次生命周期
也是从开源库看到的。
vue-element-plus-admin有个监听prop的操作,并且在弹框组件封装处配置了destroy-on-close。
大家如果有更好的办法也可以提出来~



9731

被折叠的 条评论
为什么被折叠?



