请求顺序依赖
在这种场景中,首先还是业务的复杂度决定了代码的复杂度。首先我们来看一个在前端和node都有可能出现的一个简单的例子:
我们有A,B,C,D四个请求获取数据的函数(函数自己实现),
C依赖B的结果,D依赖ABC的结果,最终输出D的结果。
错误示例:
//伪代码functionA(callbak){ajax(url,function(res){callbak(res);});}//...剩下同上//实现如下:A(function(resa){B(function(resb){C(resb,function(resc){D(resa,resb,resc,function(resd){console.log("thisisDresult:",resd);});});});});
评:emm...虽然这个代码是故意写成这样的,不过确实也有在一些初学者身上看到过。
这份代码还是能正确给出结果的,但是写法丑陋,回调地狱。如果后来人不进行重构,还有请求依赖,得继续回调嵌套。性能太差,没有考虑A和B实际上是可以并发的。
这里介绍了一下最原始的callback...中间大家可以去回顾一下整个ES+,callback(async.js)--Promise--generator+co--async+await的进化过程。其实是从原生的语法层面不断去简化和增强我们对于异步的控制能力。
下面直接给目前阶段原生提供的终极方案:基于Promise+async/await
functionA(){returnnewPromise(r=setTimeout(()={r("a");},));}//...剩下同上asyncfunctionasyncBC(){constresb=awaitB();constresc=awaitc(resb);return{resb,resc};}asyncfunctionasyncTask(){const[resa,{resb,resc}]=awaitPromise.all([A(),asyncBC()]);constresd=awaitD(resa,resb,resc);returnresd;}asyncTask().then(resd={console.log("thisisDresult:",resd);});
这里我们重新思考了一下上面的问题,理清楚了逻辑顺序的依赖。并且用最新的语法。
使用Promise.all结合async/await的形式,考虑了并发和串行,写法简洁,达到了在示例要求下的最快方案。解决了无限嵌套的问题。
这是跟随语言进化本身带给我们可以进行的优化。
但又不仅仅如此。我们将问题进行归类将B,C有依赖顺序的请求,抽离出单独的函数。让他们去处理自身的逻辑。这个点我们稍后再提。
折磨人的ifelse可能存在下面一些问题
过多的嵌套
逻辑处理冗余
没有做好防御编程(错误处理)
直接来一个代码例子,这是一个获取背景颜色的方法,但是随着业务的不断变化,背景颜色的来源越来越多,在一些业务人员的处理下可能是这样的:
const_getPageBgColor=(pageInfo,pageUrlObj)={letbgColor="";if(window.__isMyCache){if(pageInfopageInfo.theme){bgColor=pageInfo.theme.backgroundColor;}else{if(window.__customMyConfig.backgroundMap){letqueryParam=pageUrlObj.params;if(queryParam.myPid){letpids=queryParam.myPid.split("-");if(pids.length===2){bgColor=window.__customMyConfig.backgroundMap[pids[1]];}else{bgColor=window.__customMyConfig.backgroundMap[pids[0]];}}}if(!bgColorwindow.__customMyConfig.customBgMap){Object.keys(window.__customMyConfig.customBgMap).forEach(item={if(this.pageUrl.indexOf(item)0){bgColor=window.__customMyConfig.customBgMap[item];}});}}}else{if(window.__pageTheme){bgColor=window.__pageTheme.backgroundColor;}}returnbgColor;};
相信你在读上面的代码的时候是极为痛苦的,想要一目了然的知道最终会进入哪个分支,基本不可能。于是基于下面两个原则
合理的抽取成函数
错误优先返回
有了一个基础版本的重构:
constgetBackgroundMapBgColor=pageUrlObj={if(!window.__customMyConfig.backgroundMap)return"";letqueryParam=pageUrlObj.params;if(!queryParam.myPid)return"";constpids=queryParam.myPid.split("-");if(pids.length===2){returnwindow.__customMyConfig.backgroundMap[pids[1]];}returnwindow.__customMyConfig.backgroundMap[pids[0]];};constgetCustomBgMapBgColor=()={letbgColor="";if(window.__customMyConfig.customBgMap)return"";Object.keys(window.__customMyConfig.customBgMap).forEach(item={if(this.pageUrl.indexOf(item)!==-1){bgColor=window.__customMyConfig.customBgMap[item];}});returnbgColor;};const_getPageBgColor=(pageInfo,pageUrlObj)={if(!window.__isMyCache!window.__pageTheme)return"";if(window.__pageTheme){returnwindow.__pageTheme.backgroundColor;}if(pageInfopageInfo.theme){returnpageInfo.theme.backgroundColor;}letbgColor=getBackgroundMapBgColor(pageUrlObj);if(!bgColor)bgColor=getCustomBgMapBgColor();returnbgColor;};
可以看到整个逻辑,经过了重新梳理。拆分成了三个函数,子方法分别去处理对应层级的逻辑,由一个主方法负责调度。整体都变得一目了然了。
当然,在我们基于上面的原则进行重构之后,这个代码有没有问题呢?当然有。
可以看到我们这三个函数,都依赖了全局变量。函数本身就不纯了。如果是全局的问题,还是不易于排查。
需要将其修改为纯函数,才能易于理解和测试。
示例:我们将全局变量变成了参数,只需要在调用的时候,将全局变量传入即可,但是这样,我们得到了一个纯函数。
constgetBackgroundMapBgColor=(pageUrlObj,config)={if(!config.backgroundMap)return"";letqueryParam=pageUrlObj.params;if(!queryParam.myPid)return"";constpids=queryParam.myPid.split("-");if(pids.length===2){returnconfig.backgroundMap[pids[1]];}returnconfig.backgroundMap[pids[0]];};
为什么会在这里特别强调这个点呢,其实在函数式编程中的一个最基础的问题那就是纯函数。只有这样输入输出才是可被观测的,一个输入一定会有一个输出。也只有通过这样的方式,才能让系统中非纯的函数越来越少。让代码变得更易于测试。
当然作为我们如果以重构的角度去思考的话,我们还需要