当前位置: 首页 > Web前端 > vue.js

记录一个有难度的后台重构&性能优化

时间:2023-03-31 15:33:52 vue.js

有这样一个需求,原来的后台需要重构,前端显示是这样的:可以看到,这个有增删功能,需要一个很长时间了。后台给的数据格式已经简化为:{"data":{"path":"data","type":"dict","showName":"Editing文言文","value":null,"isNecessary":true,"subDefine":[{"path":"data/title","type":"string","showName":"title","value":"周亚夫君细柳","isNecessary":true,"subDefine":null},{"path":"data/book","type":"list","showName":"textbook","value":null,"isNecessary":true,"subDefine":[{"path":"data/book/book_0","type":"dict","showName":"1","value":null,"isNecessary":false,"subDefine":[{“路径”:“data/book/book_0/version","type":"string","showName":"Material","value":"JinkyoNewEdition","isNecessary":true,"subDefine":null}]},{“路径”:“数据/书/书_1”,“类型”:“dict”,“showName”:“2”,“值”:空,“isNecessary”:false,“subDefine”:[{“路径”:"data/book/book_1/version","type":"string","showName":"Material","value":"Partbook","isNecessary":true,"subDefine":null}]}]}]}}查看各个参数的含义:path:当前路径type:表示类型showName:显示的字value:输入框显示的内容isNecessary:是否必须subDefine:子元素,ifany如果没有子元素,则不会渲染。后端如何将数据传递给我?我需要以这种格式将其传递给他。中间用户可能会修改值,然后这些值需要经过验证传递给帖子。结局的背景很古老。具体是用jquery通过字符串拼接将数据拼接成想要的html,并将这些字符串插入到真实的DOM中,所以重复代码很多,而且字符串相对html写的,可读性更差。为了增强可维护性,我准备重构一下,结构图怎么画,数据怎么转换成上面的结构图,这个结构图怎么画,下面记录下我的心路历程~~:jsx获取时这个需求当时对嵌套组件的思想不是很了解,所以首先想到要不要用jsx,因为当时觉得这种嵌套结构光靠HTML是无法实现的.既然这种思路做不出来,首先想到的是能不能通过js递归调用,因为js比较灵活,最后返回一个html。这样我们就用js代替html来生成这种嵌套结构的html。然后我们需要放弃模板并使用我们自己定义的渲染函数。之前没用过jsx,先研究了一个下午,准备先做一些简单的试水,先在这个项目中尝试一些jsx代码,添加之后发现编译报错,说我缺loader,报错如下:找了一些原因,发现vue.config.js中添加了chainWebpack:(config)=>{config.module.rules.delete('js');}这样不会编译jsx,但是如果和公司自己的组件库设计有冲突,然后需要组件库成员修复这个问题,那么项目可能不能按时交付。同时,需要考虑维护成本。vue中很少有地方使用jsx。难道后来的人需要增加维护成本来维护这个?那么有没有更好的方法来解决这个问题呢?外挂懒人思维懒。我的第一反应是找个插件什么的。什么都不用管,数据就传过去了。后台本身也是用的element-ui,所以第一个想法是用tree插件,但是树组件的形状不符合产品设计的要求,但是我们看到的是需要的参数传给tree的和后台传给我的参数是差不多的,那么我们能不能从tree的实现中获取经验呢?tree组件实现原理可以参考element-ui的实现,这里简化大概就是这个意思,外面是树组件,里面是树节点,如何实现多层嵌套,下面是树节点组件的实现:可以看到这样实现嵌套组件的效果,但需要注意的是必须声明当前组件的名称,否则当前组件会在当前文件中使用。总结从tree的实现上,可以借鉴这个思路实现当前的产品需求,实现一个子组件。如果孩子存在,则调用树节点。如果它们不存在,则不需要渲染当前组件。首先我们实现了这个效果,但是这个需求并没有结束,又遇到了新的问题——性能问题。性能优化其实我们在写代码的时候很少遇到性能问题,但是这次我们确实发现,当我们向组件传递数据的时候,上面的结构渲染出来需要花费很多时间。所以有必要分析是什么导致了这些性能问题。性能分析这里借助chrome的性能分析工具performance,进行调用栈分析,看看哪个部分对性能的消耗最大。使用方法非常简单。点击这里可以对当前页面进行性能分析。以下是分析结果和具体参数。可以参考这篇chrome-performance页面性能分析使用教程,可以看到scripting占用了很多时间,旁边有一个call-tree,可以看到哪个函数占用时间最多,点下??去一一查看最耗时间的是哪个链接,最后发现如下:该方法是用在元素的textarea中自适应高度的函数。下面分析一下这个函数对性能的影响。这么大的组件优化就是我在需求中所说的autosize拖累了页面的性能,我们来看看autosize的实现。最后调用autosize的方式是resizeTextarea,我们来看看具体实现resizeTextarea(){const{autosize}=this;const{minRows,maxRows}=自动调整大小;maxRows);}可以看到最后调用的是calcTextareaHeight,具体看它的实现函数文档。body.appendChild(hiddenTextarea)}const{paddingSize,borderSize,boxSizing,contextStyle,//获取元素的大小信息}=calculateNodeStyling(targetment)//设置隐藏文本区域的样式hiddenTextarea.setAttribute('style',`${contextStyle};${HIDDEN_STYLE}`)hiddenTextarea.value=targetElement.value||targetElement.占位符||'';letheight=hiddenTextarea.scrollHeight;//包括填充高度if(boxSizing==='border-box'){height+=borderSize;}elseif(boxSizing==='content-box'){高度-=padingSize}if(hiddenTextarea.parentNode){hiddenTextarea.parentNode.remove(hiddenTextarea)}hiddenTextarea=nullreturn{height:`${height}px`}}分析上面的函数,因为组件库需要考虑的元素很多,你可以需要添加一些与我们自己的业务无关的代码。比如上面的代码中有几个地方可以针对业务进行优化:这里通过calculateNodeStyling获取元素的一些属性,对于业务来说是完全可控的。padding,border我们完全可以设置,不用js去获取。在常说的性能优化中,最重要的是避免对DOM的重复操作。如果省去这一步,效率是不是会大大提高呢?可以看这个Howtoimplementsub-adaptationheight,创建一个我们看不到的textarea,将当前textarea的样式赋值给隐藏的textarea来计算高度。这对于只需要使用简单功能的我们来说是完全没有必要的。只需使用height=scrollHeight。在代码中,我们从文档流中删除了这个隐藏的文本区域。如果文档中有1000个textarea,我们是否需要创建一个textarea并删除它?如上所述,操作DOM会导致性能下降。两个原因,所以准备做一个简单的输入框来满足我们的需求$refs.textarea;},看:value(){textarea.height=0;textarea.height=textarea.scrollHeight;}}这样可以很容易地实现输入框的高度随内容变化,并且去掉一些不必要的操作,大大提高了性能。其他性能优化除了改变上述组件的实现之外,我们还有什么地方可以优化这个需求?分配的数据响应式设置,即重新定义数据的set和get操作。但是在现在的业务中,后端的价值是一个不会变的价值。有必要我们去回应吗,这个数据量很大。如果我们递归地重新定义对这个数据的get和set操作,是不是?本身就是一个消耗性能的东西,所以我们不需要收集,而且使用object.freeze不会让Vue重新定义这些数据的setter和getter。This.data=Object.freeze(数据);这里使用了Object.freeze,下面是使用方法。例如constmap={key:'value'}map.key='12'console.log(map.key)//'value'当改变map中的key时,修改后面的value后没有变化.但是如果当前对象的属性值也是一个对象,除非对象的属性值也是一个冻结对象,那么这个对象是可以改变的,比如constmap={key:{test:'value'}}map.key.test='test'console.log(map.key.test)//测试可以看到对象冻结后,只有第一层的属性值不能改变。结合Vue源码,我们来看看这个方法的源码在上面的例子中是如何体现的,说明this.data=Object.freeze(data),在赋值数据的时候,Vue会拦截当前操作的Object.defineProperty(obj,key,{set:functionreactiveSetter(newVal){childOb=!shallow&&observe(newVal);}})functionobserve(value){//...if(Object.isExtensible(value)){ob=newObserve(value)}}满足时可见只有启用了Object.isExtensible才会响应式添加数据。Object.freeze执行时,Object.isExtensible(value)为false,所以不会重新定义set和get操作。递归组件的原理Vue本身就决定了父子创建时children的生命周期顺序:parentbeforeCreated=>parentcreated=>parentbeforeMount=>childbeforeCreated=>childcreated=>childbeforeMount=>childmounted=>parentmountedwhen更新数据的时候,父子循环的顺序是: parentbeforeUpdate->childbeforeUpdate->childupdated->parentupdated为什么渲染这么慢?因为整个组件需要等待内部子元素渲染完成。将整个父组件挂载到真实的DOM上,但是整个部分没有很好的解决方案。数据处理因为在数据处理的时候,我们遍历了后端给的每一条数据。上面的代码中,为了给一条数据添加一个需要的属性,对数组进行深度遍历,就是为了让模板中的表达式更简单。后端返回给我们的数据可能非常大,递归可能会影响性能。原则是能算的就不算。所以改用模板中的表达式来写判断条件。表达式可能很长,但可以节省性能。需求的具体实现在需求中,我们可能需要对某个元素进行子类扩展或删除,那么如何实现呢。利用Vue自身数组处理的局限性,我们知道使用Object.definePrototype无法拦截数组元素的添加和修改,所以Vue中的源码对数组进行了处理。如果我们想对数组的某个元素这样做,写Vue.$set(this.arr,key,value)这样Vue就可以监听值的变化。这也是很多小伙伴遇到的问题。明明是值变了,为什么view没变。这里也利用了这个漏洞。正如文章前面提到的,我们需要在后端保留传递给他们的数据格式。中间我们可能需要修改这些输入值,比如上面结构中的valuesubDefine:[{value:''}]代码中后端传给我的数据直接传给了组件,然后这个值相当于组件的props,Vue中禁止修改prop。

上面提到要修改数组的一个元素,必须使用Changes才能在$set的帮助下进行监控。我们这里直接改props,Vue是不会报错的。这是Vue的一个漏洞。当修改value的值时,父元素传入的props也会随之改变。至于为什么这样做,就是因为简单。如果需要考虑以后的维护,可能需要用$emit来复制对象,可能会有点麻烦。删除很简单。点击delete实际上是从父元素的subDefine中删除最后一个元素,也就是从父元素的树中删除最后一个元素。代码如下:trees.pop()这样可以删除最后一个元素。addbackend传给前端的时候,除了一个data,还有一个minData。这个minData的数据格式和data一样。不同之处在于每一项的值为空。如果item可以展开,说明可以往subDefine中添加子元素,这个subDefine不为空,但是只有一个元素。此数据在添加子元素时非常有用。例如,当前元素的subDefine为空。当我们给它添加元素的时候,此时新元素的数据结构应该是什么样的。这时候就需要找到minData中哪个元素的路径与当前路径相同。之前想过循环找找一样的,但是瞬间被自己否决了。虽然我们对算法研究的不多,但是也不能用这么low的idea。所以首先,mindData被处理。如前所述,每个元素的路径是不同的。是否可以重新创建一个对象,其中key是每条数据的路径,value是这条数据。这时,当我们需要添加新元素时,只需要知道对应的路径,然后从minData中取出key等于路径的数据,再取出该数据的subDefine的第一个数据即可.下面是minData的数据处理函数:value.subDefine,res);}});returnres;}minData=constructPathObj(data)这样就得到了一个以path为key,data为value的对象。这里还要注意一点,因为前面说的路径是唯一的,所以在添加新元素的时候不能重复路径。比如现在subDefine中有一个元素,其路径为data/book_1,而后端要求新增元素的路径为data/book_2,于是就有了如下代码const{subDefine}=item;letindex;if(subDefine.length===0){//根据路径查找子元素index=0;}else{index=subDefine.length;}consttemp=this.dealData(minData[information.path].subDefine[0],index);subDefine.push(temp);functiondealData(data,index){consttemp={};如果(data.subDefine){temp.subDefine=[];data.subDefine.forEach((val)=>{//先传给后者val.path=val.path.replace(/(.*)_[0-9]/,`$1_${data.showName}`);temp.subDefine.push(this.dealData(val));});}if(data.type==='dict'){temp.showName=index+1;temp.path=data.path.replace(/(.*)_[0-9]/,`$1_${index}`);}return{...data,...temp,};}这会标准化生成的每个元素的路径,并添加一个新元素的效果。小结因为是项目重构,所以需要一个已有的界面,界面内容不能更改。那么我们就需要修改原来的数据结构,将这些数据处理成我们想要的数据格式。在项目实施过程中,遇到了一些性能问题,分析并解决了这些问题产生的原因,加深了对需求的理解,加大了对性能优化的重视。