谨慎使用全局变量
背景
之所以写这篇文章,是因为有同事使用全局变量不当导致了bug。所以在解释标题之前,首先说一下业务背景。
很简单,就是有一个页面可以办理某个业务,这个业务又分为两种类型,可以随意切换类型。发现问题的过程是,页面初始化时默认是A类型,所以此时前端会按照A类型传参调用后台大概3个接口,我们暂且称作接口1,接口2和接口3吧。其中接口3的请求参数依赖接口1和接口2的响应参数,接口1和接口2的返回数据会展示到前端,然后调用接口3时将从接口1和接口2的返回参数中拿数据传递给接口3,然后将接口3返回的数据展示,到此页面初始化加载完成。
由下面页面草图可以看出,接口1,2,3都依赖于类型来完成对应的逻辑处理,在接口调用上肯定是先调接口1,2(二者没有先后顺序),然后调接口3。之后在从A类型切换至B类型时又会重新按B类型重新加载一遍接口1,2,3,展示B类型对应的数据。
问题排查
大概的业务规则就是这样的,很简单。但是在测试中发现,当页面初始化时,迅速切换到B类型,前端弹出一个错误窗口“系统错误,缺少必要参数”,偶现的问题但可以稳定复现。
经过排查分析发现是前端接口调用顺序问题,具体点就是调用接口3时,没有拿到需要的数据(接口3的逻辑大致是通过前端传的参数1和参数2取接口1和接口2放在缓存的数据,缓存的Key和类型有关)
从表象上看就是在调用接口3时,接口1或接口2还没有被调用,导致接口3从缓存拿不到需要的数据。
带着这样的疑问去查看前端代码,看接口的调用顺序是不是真的有问题,结果发现前端调用的顺序是没有问题的。那问题是出在哪里呢?
通过排查前端代码,发现一个问题,前端设置了一个全局变量来记录当期的业务类型(如A类型、B类型),调用接口1,2,3传递业务类型时就是传递的这个全局变量。看到这也许你就能想明白为什么说谨慎使用全局变量了,这个问题正是因为全局变量的使用不当导致的。
分析原因
我们来一起分析下到底是如何导致的吧。
上述也提到了初始化时快速切换到B类型,那么前端的这个记录当前业务类型的全局变量是何时改变其值的呢?
没错,正是在切换业务类型时记录当前业务类型A或B。当初始化默认是A类型时,接口会这样调用A类型:接口1(A)->接口2(A)->接口3(A),当切换到B类型时触发一系列接口调用,和A类型也一样,B类型:接口1(B)->接口2(B)->接口3(B)这样调用。
关键就是在切换到B类型时,可能会存在这样的问题,接口1,2正常调用,即传递的业务类型都是A,但恰好在调用接口3前,切换到了业务类型B类型,那么此时记录当前业务类型的全局变量随之变为B,那么此时原本初始化的时候的接口3拿到的业务类型就由预期的A变成了B,而在此之前接口1,2都是按A类型传递的参数,故后台存储的数据是A类型的,但此时因为全局变量的变化,接口3传递的业务类型就又A变为B,故在接口3的业务逻辑里,按业务类型B去缓存取数据时是取不到,后端校验参数时就会报错“系统错误,缺少必要参数”。
看到这,你是不是觉得这有点像java的多线程共享变量?多线程共享变量也会引发这样的问题,当一个线程正在使用某一变量时,突然被别的线程修改了,导致该线程拿到了脏数据。解决办法是,线程独享资源的操作权,操作完毕其他线程才有权限读取该资源,同一时间只有一个线程才能修改共享变量,即多个线程间相对于该资源是互斥的关系,java中多用锁来保证操作的安全性。
那在这个问题中,怎么类比呢?我们可以把选中A类型时要走的一系列接口比作A线程;把B类型要走的一系列接口比作B线程,这两个线程执行的流程、方法一样,只是需要的参数的具体值是不一样的,A、B线程各自执行三个步骤每个步骤都会取共享变量作为参数传递给后台。再把切换类型要改变当前业务类型(`biz_type`)这一操作记作C线程。那大致就是,A、B线程读 `biz_type` ,C线程修改 `biz_type` 。这就可以理解成三个线程共享一个变量,在页面上切换业务类型可以看做线程的轮转,所以不加以控制难免会发生错误。
问题解决
弄懂了发生问题的原因之后怎么来解决呢?其实解决起来也简单,正如标题所说[**谨慎使用全局变量**],问题的根源就是使用了全局共享变量,导致在A线程还没走完时C线程修改了 biz_type 的值,从而导致线程A的三个步骤拿到的 biz_type 的值不相同,进而导致后台根据类型取缓存数据时拿不到,最终报错。所以,想要解决该问题,最关键的就是从这个全局变量着手,经查看前端代码而知:在切换类型时,根据当前选中的类型传递相应的参数,当选中时我们就能知道是哪种类型了,所以我们就能清楚的去调用接口传递相应的类型字段,而不是先对全局变量赋值,再在接口里自行去取全局变量。
修改前:
var biz_type = 'A';//定义全局变量,默认为A业务类型 //change radio function changeRadio(){ if(#('#bizType_A').is(':checked')){ biz_type = 'A';//修改变量值 api_1(); }else{ biz_type = 'B';//修改变量值 api_1(); } } //function1 function api_1(){ //get biz_type //send ajax with biz_typ if(data.success){ api_2(); }else{ alert(data.msg); } } //function2 function api_2(){ //get biz_type //send ajax with biz_typ if(data.success){ api_3(); }else{ alert(data.msg); } } //function3 function api_3(){ //get biz_type //send ajax with biz_type if(data.success){ jump_to_success(); }else{ alert(data.msg); } }
修改后:
//change radio function changeRadio(){ if(#('#bizType_A').is(':checked')){ api_1('A');//参数传递 }else{ api_1('B');//参数传递 } } //function1 function api_1(biz_type){ //send ajax with biz_typ if(data.success){data. api_2(biz_type); }else{ alert(data.msg); } } //function2 function api_2(biz_type){ //send ajax with biz_typ if(data.success){ api_3(biz_type); }else{ alert(data.msg); } } //function3 function api_3(biz_type){ //send ajax with biz_type if(data.success){ jump_to_success(); }else{ alert(data.msg); } }
修改后使用参数传递的方式,这样可以保证一套流程走下来,拿到的 biz_type 值一样。
另外,可以通过控制切换的方式保证A线程没走完时不允许修改 biz_type 的值,不允许执行B线程,即当A类型下的流程没走完时切换不了类型。可以通过标志位来判定A流程是否走完,进而判定是否可以切换到B类型上。
总结
不过这个问题不大,后端做了参数的校验,但是为了提升用户体验这个问题一定是要解决的。这其实是前端开发人员一个小小的疏忽导致的,当前端在写代码时他肯定不会预见到会发生这样的问题,他肯定不会想到全局变量会导致这样的问题,更不会想到用户在页面没初始化完成时就切换类型。但这些对于一个初出茅庐的前端开发来说,情有可原,权当是积累经验了。切记能传参的尽量不要用全局变量
出问题不可怕,在问题中成长,积累经验,才是最重要的。