Author: Jiang, Jilin
在网页开发过程中,我们时常会遇到当数据量很大,或者页面同时渲染过多内容导致页面等待时间太久或者动画效果不流畅甚至假死的问题。那么我们如何避免因此造成的页面影响呢?接下来,我就将通过一些例子来提出页面的改进意见:
一 最后触发动画
jQuery中拥有animate方法,可以十分便捷的做出动画效果。但是有时候会发现动画效果并不流畅,会出现卡顿现象。这是由于触发动画效果后,执行了比较耗时的操作(例如大量的dom元素操作、复杂的循环等等),使得动画函数执行到下一个interval时被滞后。当多个interval被滞后时,就会出现动画的开始部分被吃掉了。比如下面这种情况,页面将不会刷新直到耗时操作结束:
将animate方法放在函数最后,让耗时操作先于动画执行,可以获得平滑的动画效果。同样的,思考一下如下的代码(其中,do something为耗时操作):
与:
经过之前的说明,我们就很容易分析出这段代码片段执行的时间序列:
方法1:
时间-0: for loop
时间-1: (do something...)*1000 times
方法2:
时间-0: doAction2
时间-1: do something...
时间-2: setTimeout
时间-3: doAction2
...
因而方法1对于方法2而言,在页面表现上会更卡一些。因为它在同一个时间片中将耗时操作进行了1000次。而方法2则由于将耗时操作分于不同的时间片上执行,使得游览器获得了更多的渲染次数。从而显得“不那么卡”了。
二 并列加载与依次加载
在app当中,我们时常会使用ajax来加载数据。有时候可能会是多个请求同时进行。这时我们会用到jQuery的when方法。将多个请求同时返回再执行操作。但是如果其中的某一个request耗时十分巨大,那就会导致页面上出现很大的延迟。会让人造成一种页面假死的错觉。
对于这种情况而言,我们需要分析同时发出的请求是否真的拥有并列关系,还是实际上他们是依赖关系。对于依赖关系的请求,往往要求同时返回就没有那么合理了。接下来,我们思考一下如下的例子。思考一下它们的关系是什么:
我们有3个web service,它们的作用分别如下:
A 提供了所有用户的信息:[{id,username,age}]
B 提供了所有黑名单用户的信息:[{id,username}]
C 提供了聚合后的当月所有用户信息[{id, cost}](如果当月有用户没有任何消费,则不会返回该用户的记录)
其中,消耗的时间:C>B>A。结果集数量:A>B,A>C。
现在,我们要求制作一个页面。把所有的用户按照“姓名”和“消费”以表格形式列出来,并且高亮黑名单用户的“姓名”。我们该怎么做呢?
使用同步操作:
可以看出,正常的情况下。回调返回依赖于C返回的时间。
考虑这三者之间的关系,我们很容易就看出来:B依赖A,C依赖A。而BC之间没有关系。
因而可以做成如下形式:
代码执行的时间序列:
方法1:
时间-0: $requestA, $requestB, $requestC
时间-1: dataA
时间-2: dataB
时间-3: dataC, render table...
方法2:
时间-0: $requestA
时间-1: dataA, render table..., $requestB, $requestC
时间-2: dataB, do something...
时间-3: dataC, do something...
通过异步渲染,可以使得用户在时间-1时就获得页面更新。从而提升用户体验。
三 载入交互
在使用ajax请求的地方使用显示的载入动画元素。动画元素可以分散用户的注意力,从而减少数据载入的等待感:
1 避免使用复杂的载入动画
一般而言,复杂的载入动画较简易的载入动画更加吸引用户注意力。然而有时会出现载入动画还没有播放完毕,页面已经载入完成。对于正在欣赏动画的用户而言,这可不是好事儿。
2 非阻塞式等待
当页面显示载入动画时,例如弹出框。如果是非不可逆操作,务必允许用户可以关闭该弹出框。(同时,出于网页的安全性和稳定性考虑,你应该尽量减少不可逆操作。如果出于某种原因,你必须使用不可逆操作。你必须考虑到如果用户出于某种原因,刷新了页面场景还原的问题。)
3 甚至于,你可以直接制造一个伪进度条
由于ajax请求无法获知当前的进度。因而你可以人为制作一个伪进度条。评估出载入平均时间,制作一个需要其两倍时间的伪进度条。使用一定曲线的动画方式增长进度条。可以减少用户的等待感。
4 重试与提示
当通过ajax请求时,务必处理错误状态。你可以选择重试请求(如果耗时过长,务必在页面上标明以让用户知道目前的状态),或者直接使用简单明了的说明告知用户请求失败并询问是否需要重新请求。
四 本地缓存js文件
在打开网页的时候,游览器会依次查询并载入对应的资源文件。那如果我们节省了网络请求和资源下载的时间,是不是网页载入速度会更快了呢?这里就不得不提html5的localStorage。它的作用是允许网页将部分数据存储于本地。因而我们可以将js文件载入到localStorage。等用户在此打开页面时,查询本地是否已经有缓存了js文件,如果有则载入本地缓存。这里推荐basket.js,一个简单轻巧的localStorage cache库。
五 更多的使用局部变量
js中,如果一个变量没有申明就直接使用,这个变量就会变成全局变量。这很容易造成变量混淆的情况。因而,我们在使用变量时,最好总是提前进行申明。此外,我们也需要尽量避免使用全局变量。考虑以下代码的区别:
与:
两者看似相近,甚至效果似乎也相同,只是一行代码的位置变了。但是其区别还是很大的。
前者将count变量申明成了全局变量。因而我在任何地方都可以对这个count进行赋值。这是很危险的事情,很可能由于两个不同的js文件使用了同一个全局变量,导致页面行为的异常。而后者的count变量对外界是不可见的。除非主动暴露,否则function之外的代码是无法修改它的。
这里就提到了一个作用域的概念。在js中,变量的作用域是函数作用域而不是块作用域。因而在块中申明的代码,其外部同样可以使用。如:
这里需要注意的是,变量在方法内部任何地方申明,其作用域都是该函数。接着我们调换一下形式,看看console打印的分别是多少呢?
// undefined 变量a在函数内部申明,但是没有赋值因而是undefind
// 2 本地变量被赋值为2
// 3 本地变量被赋值为3
// 3 本地变量被赋值为3
// 1 全局变量值保持不变
接下来,我们来考虑一些更复杂的情况:
下面这段代码的目的是发出10个ajax请求并且将对应的结果存在一个list数组中。请问有什么问题么?
以上这段代码的问题在于i是公用变量。异步请求返回后所有的list[i]都会指向list[9]。导致list[9]被反复填充数据。那么我们如何修改呢?
前面已经说明了,js的作用域是函数作用域。因而function内部声明的变量将不受外界影响。通过var j = i将i数值存下,待ajax返回后通过j赋值。
如果在深入下去,就涉及到了闭包的概念。那又是另一个故事了……