Cookbook组件形式:优化Vue组件的运行时性能
Vue 2.0在发布时以其出色的运行时性能而闻名。您可以通过这个第三方基准来比较其他框架的性能。Vue使用虚拟DOM来呈现视图。当数据发生变化时,Vue会比较前后两个组件树,只同步视图的必要更新。
Vue为我们做了很多,但是对于一些复杂的场景,尤其是大量的数据渲染,我们要时刻关注应用的运行时性能。
本文遵循Vue Cookbook的组织形式,描述了Vue组件运行时性能的优化。
基本示例
在下面的示例中,我们开发了一个树控件,它支持基本的树结构显示和节点展开和折叠。
我们将树组件的接口定义如下。数据绑定到树控件的数据,树控件是由几棵树组成的数组,子节点代表子节点。扩展键绑定扩展节点的键属性,使用sync修饰符同步组件触发的节点扩展状态的更新。
模板树: data=' data ' expanded-keys . sync=' expanded keys '/tree/templatescript export default { data(){ return { data :[{ key : ' 1 ',Label: '节点1 ',Children: [{key3360' 1-1 ',label3360 '节点1-1'}]},{key:' 2 ',label : '节点2 ' } } } };/scriptTree组件的实现如下,这是一个稍微复杂的例子,需要阅读几分钟。
模板ul class=' tree ' Li v-for=' nodes ' v-show=' status[node . key]中的节点。visible ' : key=' node . key ' class=' tree-node ' : style=' { ' padding-left ' : ` $ { node . level * 16 } px ` } ' I v-if=' node . children ' class=' tree-node-arrow ' : class=' { expanded : status[node . key]。expanded } ' @ click=' change expanded(node . key)'/I { { node . label } }/Li/ul/templatescript export default { prop : { data : Array,expanded keys : { type 3: Array,default: ()=[],},},computed: {//将数据转换为一维数组,便于v-for遍历//添加级别和父属性节点(){Return this。getnodes(这个。数据);},//status是节点status(){ return this . getstatus(this . nodes)的一个键和一个Map数据结构;},},methods: {//递归数据并返回所有节点的一维数组getNodes(数据,级别=0,父级=null){ let Nodes=[];data . foreach((item)={ const node={ level,parent,item,};nodes.push(节点);if(item . children){ const children=this . getnodes(item . children,level 1,node);节点=[.节点,儿童];node . children=children . filter(child=child . level===level 1);} });返回节点;}、//遍历节点并计算每个节点的状态get status(nodes){ const status={ };nodes . foreach((node)={ const parent status=status[node . parent node . parent . key]| | { };status[node . key]={ expanded : this . expanded keys . includes(node . key),visible : node . level===0 | |(parent status . expanded parent status . visible),};});返回状态;},//切换节点的展开状态更改展开(键){const index=this。扩展键。(键)的索引;const expandedKeys=[.this . expandedkeys];if(index=0){ expandedkeys . splice(index,1);} else { expandedkeys . push(key);}这个。$emit('update:expandedKeys ',expanded keys);}, },};/script在展开或折叠节点时,我们只需要更新展开的-key,状态计算属性会自动更新,以确保关联子节点的可见状态正确。
一切准备就绪。为了衡量Tree组件的运行性能,我们设置了两个指标。
初始渲染时间节点展开/折叠时间
将以下代码添加到树组件中,并使用console.time和console.timeEnd来输出操作的特定时间消耗。
导出默认值{ //.methods: { //.changeExpanded(key) { //.这个。$emit('update:expandedKeys ',expanded keys);console.time('扩展更改');这个。$nextTick(()={ console.timeEnd('扩展更改');});},},beforeCreate() { console.time('第一次呈现');},mounted() { console.timeEnd('第一次呈现');},};同时,为了放大可能出现的性能问题,我们编写了一个生成可控节点数据量的方法。
template tree : data=' data ' : expanded-keys . sync=' expanded keys '/tree/templatescriptexportdefault { data(){ return }//生成一个包含3层的模板。十个节点树,每层有1000个节点。数据:getrandom data (3,10),扩展键3360 [],};},methods: { getRandomData(图层,计数,父项){ return array . from({ length : count },(v,i)={ const key=(父项?`$ { parent . key }-` : ' ')(I 1);Const node={key,label: node ${key} `,};if(layers 1){ node . children=this . getrandomdata(layers-1,count,node);}返回节点;});}, },};脚本您可以通过这个完整的CodeSandbox示例实际观察性能损失。点击箭头展开或折叠一个节点,在Chrome DevTools的控制台输出如下(不要使用CodeSandbox的控制台,不准确)。
在第一次渲染:上,第一次渲染需要400 ms,在作者的低功耗笔记本下扩展或折叠节点需要200 ms。让我们优化一下Tree组件的性能。33335 . 33333733336
如果你的设备性能很强,可以修改生成的节点数,例如this.getRandomData(4,10)生成10,000个节点。
使用Chrome Performance查找性能瓶颈
Chrome的Performance面板可以记录一段时间内js的执行细节和时间。使用Chrome开发工具分析页面性能。
打开Chrome开发工具,切换到“性能”面板,单击“录制”开始录制并刷新页面,或者展开一个节点并单击“停止”停止录制
控制台时间输出的值也将显示在性能中,以帮助我们进行调试。有关性能的更多信息,请点击此处。
优化运行时性能
条件渲染
向下看性能分析结果,我们发现大部分时间都花在了渲染函数上,下面还调用了很多其他函数。
当遍历节点时,我们使用v-show指令来显示节点,不可见的节点也被渲染,然后通过样式使其不可见。因此,我们尝试使用v-if指令进行条件渲染。
Li v-for='节点' v-if='状态[node.key]中的节点。visible ' : key=' node . key ' class=' tree-node ' : style=' { ' padding-left ' : ` $ { node . level * 16
可见吗?H('li') :这个。_e() //这个。_e()生成一个标注节点,即v-if,这样只减少了每次遍历的时间,却没有减少遍历的次数。而且Vue.js样式指南明确指出,v-if和v-for不应该同时用在同一个元素上,因为这可能会导致不必要的渲染。
相反,我们可以遍历可见节点的计算属性:
Li v-for=' visioblenodes中的节点' : key=' node . key ' class=' tree-node ' : style=' { ' padding-left ' : ` $ { node . level * 16 } px ` } './liscripttexport {//.computed : { visibleNodes(){ return this . nodes . filter(node=this . status[node . key])。可见);}, }, //.}/脚本优化性能需要以下时间:
第一次渲染3360 194.7890625 ms扩展变化3360.0100426875 ms你可以通过改进的示例(Demo2)观察组件的性能损失,相比优化前有很大的提升。
双向绑定
在前面的例子中,我们使用了。同步到“绑定”扩展键,这实际上是prop和自定义事件的语法糖。这样便于Tree的父组件同步更新扩展状态。
但是,在使用树组件时,如果不传递展开的键,即使不关心展开或折叠操作,节点也不能展开或折叠。扩展键被认为是外界的副作用。
!-无法展开/折叠节点-tree:data=' data'/tree。这里仍然存在一些性能问题。当展开或折叠节点时,它会触发父组件的副作用来更新展开的键。树组件的状态取决于展开的键,因此将调用这个. getStatus方法来获取新的状态。即使只有单个节点的状态改变,也会导致所有节点的状态重新计算。
我们将状态视为树组件的内部状态,并在展开或折叠节点时直接修改状态。同时,定义了默认的扩展节点default-expanded-key。状态仅取决于初始化期间的默认扩展键。
export default { prop : { data : array,defaultexpandedkeys:默认扩展节点{type:array,default3360 ()=[],},},data(){ return { status 3360 null,//status为local };},computed : { nodes(){ return this . getnodes(this . data);},},watch : { node s 3360 {//当节点改变{this。状态=这个。getstatus(这。节点);},//初始化statusimmediate : true,},//重新计算statusdefaultexpandedeys(){ this。状态=这个。getstatus(这。节点);},},methods: {getnodes(数据,级别=0,父级=null) {//.},getstatus (nodes) {//.},//展开或折叠节点时直接修改状态,通知父组件change expanded(key){ console . time(' expandconst node=this . nodes . find(n=n . key===key);//找到节点const newExpanded=!this.status[key]。扩大了;//新的扩展状态//递归此节点的后代节点,更新状态常量更新可见=(n,可见)={ n . children . foreach((child)={ this . status[child . key]。visible=visible this . status[n . key]。扩大了;if(child . children)updateVisible(child,visible);});};this.status[key]。expanded=newExpandedupdateVisible(节点,new expanded);//触发此事件的节点扩展状态更改。$ emit ('expanded-change ',节点,新建扩展,this . nodes . filter(n=this . status[n . key])。扩大的));这个。$nextTick(()={ console.timeEnd('扩展更改');});},},beforeCreate() { console.time('第一次呈现');},mounted() { console.timeEnd('第一次呈现');},};使用树组件时,即使没有传递默认扩展键,节点也可以正常扩展或折叠。
!-可以展开或折叠节点-tree:data=' data'/tree!-配置默认的扩展节点-tree : data=' data ' : default-expanded-keys='[' 1 ',' 1-1 ']' @ expanded-change=' handleexpanded change '/tree的优化性能时间如下。
首先渲染3360 91.4819359375毫秒扩展变化3428775毫秒你可以通过改进的例子(Demo3)观察到组件的性能损失。34376.49393939336
冻结数据
到目前为止,Tree组件的性能问题还不明显。为了进一步扩展性能问题,找到优化空间。我们将节点数量增加到10000个。
//生成10,000个节点这个. getRandomData(4,1000)在这里,我们特意创建了一个可能会有性能问题的更改。虽然这不是必须的,但可以帮助我们理解接下来要介绍的问题。
修改计算出的属性节点,得到数据观察器中节点的值。
导出默认值{ //.watch : { data : { handler(){ this . nodes=this . getnodes(this . data);this . status=this . GetStatus(this . nodes);},immediate: true,},//.}, //.};这种修改对实现的功能没有影响,那么性能呢?
在第一次渲染:119140625毫秒的扩展变化时,使用性能工具,尝试找到性能瓶颈。58860 . 88888888886
我们发现在调用getNodes方法后,会出现一个耗时的proxySetter。这是Vue给nodes属性添加了一个响应,这样Vue就可以跟踪依赖关系的变化。GetStatus也是如此。
当您将一个普通的JavaScript对象传递给Vue实例的数据选项时,Vue将遍历这个对象的所有属性,并使用Object.defineProperty将所有这些属性变成getter/setter。
对象越复杂,层次越深,过程就越长。当我们有1w个节点时,proxySetter的时间会很长。
这里有一个问题,我们不会修改节点的特定属性,而是在每次数据发生变化时重新计算。因此,这里为节点添加的响应公式是无用的。那么如何去掉不必要的proxySetter呢?一种方法是将节点改回计算属性,该属性通常没有赋值行为。另一种方法是冻结数据。
使用Object.freeze()冻结数据,这将阻止修改现有属性,这意味着响应系统无法再跟踪更改。
this . nodes=object . freeze(this . getnodes(this . data));查看性能工具,在获取节点方法之后没有代理。
性能指标如下,可以显著改善初始渲染。
在第一次渲染:29808875毫秒扩展变化3933875毫秒的时候,你可以通过改进的例子(Demo4)观察组件的性能损失。58960 . 88888888886
我们能以同样的方式优化状态跟踪吗?答案是否定的,因为我们需要更新状态中的changeExpanded属性值。因此,这种优化只适用于属性不会更新的数据,而只会更新整个对象。而且结构越复杂,层次越深,优化效果越明显。
比较方案
我们可以看到,无论是节点的渲染还是数据的计算,都存在大量的循环或递归。对于这么大的数据量,除了上面提到的针对Vue的优化,我们还可以从两个方面进行优化:减少每个周期的时间消耗,减少周期数。
例如,您可以使用字典来优化数据查找。
//生成Map对象const expandedkeysmap=this . defaultexpandkeys . reduce((Map,key)={ Map[key]=true;返回地图;}, {});//if(expandedKeysMap[key])的事件复杂度{//do某物} defaultexpandedkeyes。包括为0(n),扩展的key map[key]的时间复杂度为0(1)。
有关优化Vue应用程序性能的更多信息,请参见Vue应用程序性能优化指南。
这样做的价值
应用程序性能对于改善用户体验非常重要,但经常被忽视。想象一下,在一台设备上运行良好的应用程序到达另一台配置不佳的设备时,会使用户的浏览器崩溃,这一定是一种糟糕的体验。或者你的应用在正常数据下运行正常,但是在大数据量下需要很长的等待时间,所以你可能会错过一些用户。
摘要
性能优化是一个由来已久的话题,没有通用的方法可以解决所有的性能问题。性能优化可以进行不当,但随着问题的深入,性能瓶颈会越来越不明显,优化也会越来越难。
本文中的例子具有一定的特殊性,但它为我们提供了性能优化的方法。
确定衡量运行时性能的指标,确定优化目标,比如实现1W数据的Chrome Performance,分析性能问题,优先解决大问题(瓶颈),重复3、4步,直到达到目标。以上是边肖介绍的Cookbook组件形式:优化Vue组件的运行时性能,希望对大家有所帮助。如果你有任何问题,请给我留言,边肖会及时回复你的!
版权声明:Cookbook组件形式:优化Vue组件的运行时性能是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。