线上问题
本周五下午的时候,正在排查一个打包后 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
。
大家如果有更好的办法也可以提出来~