前端性能优化总结
目录
页面加载优化
一、减少HTTP请求
上面说到80%~90%时间花在了下载页面中的所有组件进行的HTTP请求上。因此,改善响应时间最简单的途径就是减少HTTP请求的数量。
图片地图:
假设导航栏上有五幅图片,点击每张图片都会进入一个链接,这样五张导航的图片在加载时会产生5个HTTP请求。然而,使用一个图片地图可以提高效率,这样就只需要一个HTTP请求。
服务器端图片地图:将所有点击提交到同一个url,同时提交用户点击的x、y坐标,服务器端根据坐标映射响应
客户端图片地图:直接将点击映射到操作
<img src="planets.jpg" border="0" usemap="#planetmap" alt="Planets" />
<map name="planetmap" id="planetmap">
<area shape="rect" coords="180,139,14" href ="venus.html" alt="Venus" />
<area shape="rect" coords="129,161,10" href ="mercur.html" alt="Mercury" />
<area shape="rect" coords="0,0,110,260" href ="sun.html" alt="Sun" />
<area shape="rect" coords="140,0,110,260" href ="star.html" alt="Sun" />
</map>
使用图片地图的缺点:指定坐标区域时,矩形或圆形比较容易指定,而其它形状手工指定比较难
CSS Sprites
CSS Sprites直译过来就是CSS精灵,但是这种翻译显然是不够的,其实就是通过将多个图片融合到一副图里面,然后通过CSS的一些技术布局到网页上。特别是图片特别多的网站,如果能用css sprites降低图片数量,带来的将是速度的提升。
<div>
<span id="image1" class="nav"></span>
<span id="image2" class="nav"></span>
<span id="image3" class="nav"></span>
<span id="image4" class="nav"></span>
<span id="image5" class="nav"></span>
</div>
.nav {
width: 50px;
height: 50px;
display: inline-block;
border: 1px solid #000;
background-image: url('E:/1.png');
}
#image1 {
background-position: 0 0;
}
#image2 {
background-position: -95px 0;
}
#image3 {
background-position: -185px 0;
}
#image4 {
background-position: -275px 0;
}
#image5 {
background-position: -366px -3px;
}
运行结果:
PS:使用CSS Sprites还有可能降低下载量,可能大家会认为合并后的图片会比分离图片的总和要大,因为还有可能会附加空白区域。实际上,合并后的图片会比分离的图片总和要小,因为它降低了图片自身的开销,譬如颜色表、格式信息等。
字体图标
在可以大量使用字体图标的地方我们可以尽可能使用字体图标,字体图标可以减少很多图片的使用,从而减少http请求,字体图标还可以通过CSS来设置颜色、大小等样式,何乐而不为。
合并脚本 和样式表
将多个样式表或者脚本文件合并到一个文件中,可以减少HTTP请求的数量从而缩短效应时间。
然而合并所有文件对许多人尤其是编写模块化代码的人来说是不能忍的,而且合并所有的样式文件或者脚本文件可能会导致在一个页面加载时加载了多于自己所需要的样式或者脚本,对于只访问该网站一个(或几个)页面的人来说反而增加了下载量,所以大家应该自己权衡利弊。
二、使用CDN
如果应用程序web服务器离用户更近,那么一个HTTP请求的响应时间将缩短。另一方面,如果组件web服务器离用户更近,则多个HTTP请求的响应时间将缩短。
CDN(内容发布网络)是一组分布在多个不同地理位置的Web服务器,用于更加有效地向用户发布内容。在优化性能时,向特定用户发布内容的服务器的选择基于对网络慕课拥堵的测量。例如,CDN可能选择网络阶跃数最小的服务器,或者具有最短响应时间的服务器。
CDN还可以进行数据备份、扩展存储能力,进行缓存,同时有助于缓和Web流量峰值压力。
CDN的缺点:
1、响应时间可能会受到其他网站流量的影响。CDN服务提供商在其所有客户之间共享Web服务器组。
2、如果CDN服务质量下降了,那么你的工作质量也将下降
3、无法直接控制组件服务器
三、添加Expires头
页面的初次访问者会进行很多HTTP请求,但是通过使用一个长久的Expires头,可以使这些组件被缓存,下次访问的时候,就可以减少不必要的HTPP请求,从而提高加载速度。
Web服务器通过Expires头告诉客户端可以使用一个组件的当前副本,直到指定的时间为止。例如:
Expires: Fri, 18 Mar 2016 07:41:53 GMT
Expires缺点: 它要求服务器和客户端时钟严格同步;过期日期需要经常检查
HTTP1.1中引入Cache-Control来克服Expires头的限制,使用max-age指定组件被缓存多久。
Cache-Control: max-age=12345600
若同时制定Cache-Control和Expires,则max-age将覆盖Expires头
四、压缩组件
从HTTP1.1开始,Web客户端可以通过HTTP请求中的Accept-Encoding头来表示对压缩的支持
Accept-Encoding: gzip,deflate
如果Web服务器看到请求中有这个头,就会使用客户端列出来的方法中的一种来进行压缩。Web服务器通过响应中的Content-Encoding来通知 Web客户端。
Content-Encoding: gzip
代理缓存
当浏览器通过代理来发送请求时,情况会不一样。假设针对某个URL发送到代理的第一个请求来自于一个不支持gzip的浏览器。这是代理的第一个请求,缓存为空。代理将请求转发给服务器。此时响应是未压缩的,代理缓存同时发送给浏览器。现在,假设到达代理的请求是同一个url,来自于一个支持gzip的浏览器。代理会使用缓存中未压缩的内容进行响应,从而失去了压缩的机会。相反,如果第一个浏览器支持gzip,第二个不支持,你们代理缓存中的压缩版本将会提供给后续的浏览器,而不管它们是否支持gzip。
解决办法:在web服务器的响应中添加vary头Web服务器可以告诉代理根据一个或多个请求头来改变缓存的响应。因为压缩的决定是基于Accept-Encoding请求头的,因此需要在vary响应头中包含Accept-Encoding。
vary: Accept-Encoding
五、将样式表放在头部
首先说明一下,将样式表放在头部对于实际页面加载的时间并不能造成太大影响,但是这会减少页面首屏出现的时间,使页面内容逐步呈现,改善用户体验,防止“白屏”。
我们总是希望页面能够尽快显示内容,为用户提供可视化的回馈,这对网速慢的用户来说是很重要的。
将样式表放在文档底部会阻止浏览器中的内容逐步出现。为了避免当样式变化时重绘页面元素,浏览器会阻塞内容逐步呈现,造成“白屏”。这源自浏览器的行为:如果样式表仍在加载,构建呈现树就是一种浪费,因为所有样式表加载解析完毕之前务虚会之任何东西
六、将脚本放在底部
更样式表相同,脚本放在底部对于实际页面加载的时间并不能造成太大影响,但是这会减少页面首屏出现的时间,使页面内容逐步呈现。
js的下载和执行会阻塞Dom树的构建(严谨地说是中断了Dom树的更新),所以script标签放在首屏范围内的HTML代码段里会截断首屏的内容。
下载脚本时并行下载是被禁用的——即使使用了不同的主机名,也不会启用其他的下载。因为脚本可能修改页面内容,因此浏览器会等待;另外,也是为了保证脚本能够按照正确的顺序执行,因为后面的脚本可能与前面的脚本存在依赖关系,不按照顺序执行可能会产生错误。
七、避免CSS表达式
CSS表达式是动态设置CSS属性的一种强大并且危险的方式,它受到了IE5以及之后版本、IE8之前版本的支持。
p {
width: expression(func(),document.body.clientWidth > 400 ? "400px" : "auto");
height: 80px;
border: 1px solid #f00;
}
<p><span></span></p>
<p><span></span></p>
<p><span></span></p>
<p><span></span></p>
<p><span></span></p>
<script>
var n = 0;
function func() {
n++;
// alert();
console.log(n);
}
</script>
鼠标移动了几次,函数的运行次数轻而易举的达到了几千次,危险性显而易见。
如何解决:
一次性表达式:
p {
width: expression(func(this));
height: 80px;
border: 1px solid #f00;
}
<p><span></span></p>
<p><span></span></p>
<p><span></span></p>
<p><span></span></p>
<p><span></span></p>
<script>
var n = 0;
function func(elem) {
n++;
elem.style.width = document.body.clientWidth > 400 ? '400px' : "auto";
console.log(n);
}
</script>
事件处理机制
用js事件处理机制来动态改变元素的样式,使函数运行次数在可控范围之内。
八、使用外部的JavaScript和CSS
内联脚本或者样式可以减少HTTP请求,按理来说可以提高页面加载的速度。然而在实际情况中,当脚本或者样式是从外部引入的文件,浏览器就有可能缓存它们,从而在以后加载的时候能够直接使用缓存,而HTML文档的大小减小,从而提高加载速度。
影响因素:
1、每个用户产生的页面浏览量越少,内联脚本和样式的论据越强势。譬如一个用户每个月只访问你的网站一两次,那么这种情况下内联将会更好。而如果该用户能够产生很多页面浏览量,那么缓存的样式和脚本将会极大减少下载的时间,提交页面加载速度。
2、如果你的网站不同的页面之间使用的组件大致相同,那么使用外部文件可以提高这些组件的重用率。
加载后下载
有时候我们希望内联样式和脚本,但又可以为接下来的页面提供外部文件。那么我们可以在页面加载完成止呕动态加载外部组件,以便用户接下来的访问。
1 function doOnload() {
2 setTimeout("downloadFile()",1000);
3 }
4
5 window.onload = doOnload;
6
7 function downloadFile() {
8 downloadCss("http://abc.com/css/a.css");
9 downloadJS("http://abc.com/js/a.js");
10 }
11
12 function downloadCss(url) {
13 var ele = document.createElement('link');
14 ele.rel = "stylesheet";
15 ele.type = "text/css";
16 ele.href = url;
17
18 document.body.appendChild(ele);
19 }
20
21 function downloadJS(url) {
22 var ele = document.createElement('script');
23 ele.src = url;
24 document.body.appendChild(ele);
25 }
在该页面中,JavaScript和CSS被加载两次(内联和外部)。要使其正常工作,必须处理双重定义。将这些组件放到一个不可见的IFrame中是一个比较好的解决方式。
九、减少DNS查找
当我们在浏览器的地址栏输入网址(譬如: www.linux178.com) ,然后回车,回车这一瞬间到看到页面到底发生了什么呢?
域名解析 --> 发起TCP的3次握手 --> 建立TCP连接后发起http请求 --> 服务器响应http请求,浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) --> 浏览器对页面进行渲染呈现给用户
域名解析是页面加载的第一步,那么域名是如何解析的呢?以Chrome为例:
1. Chrome浏览器 会首先搜索浏览器自身的DNS缓存(缓存时间比较短,大概只有1分钟,且只能容纳1000条缓存),看自身的缓存中是否有www.linux178.com 对应的条目,而且没有过期,如果有且没有过期则解析到此结束。 注:我们怎么查看Chrome自身的缓存?可以使用 chrome://net-internals/#dns 来进行查看 2. 如果浏览器自身的缓存里面没有找到对应的条目,那么Chrome会搜索操作系统自身的DNS缓存,如果找到且没有过期则停止搜索解析到此结束. 注:怎么查看操作系统自身的DNS缓存,以Windows系统为例,可以在命令行下使用 ipconfig /displaydns 来进行查看 3. 如果在Windows系统的DNS缓存也没有找到,那么尝试读取hosts文件(位于C:\Windows\System32\drivers\etc),看看这里面有没有该域名对应的IP地址,如果有则解析成功。 4. 如果在hosts文件中也没有找到对应的条目,浏览器就会发起一个DNS的系统调用,就会向本地配置的首选DNS服务器(一般是电信运营商提供的,也可以使用像Google提供的DNS服务器)发起域名解析请求(通过的是UDP协议向DNS的53端口发起请求,这个请求是递归的请求,也就是运营商的DNS服务器必须得提供给我们该域名的IP地址),运营商的DNS服务器首先查找自身的缓存,找到对应的条目,且没有过期,则解析成功。如果没有找到对应的条目,则有运营商的DNS代我们的浏览器发起迭代DNS解析请求,它首先是会找根域的DNS的IP地址(这个DNS服务器都内置13台根域的DNS的IP地址),找打根域的DNS地址,就会向其发起请求(请问www.linux178.com这个域名的IP地址是多少啊?),根域发现这是一个顶级域com域的一个域名,于是就告诉运营商的DNS我不知道这个域名的IP地址,但是我知道com域的IP地址,你去找它去,于是运营商的DNS就得到了com域的IP地址,又向com域的IP地址发起了请求(请问www.linux178.com这个域名的IP地址是多少?),com域这台服务器告诉运营商的DNS我不知道www.linux178.com这个域名的IP地址,但是我知道linux178.com这个域的DNS地址,你去找它去,于是运营商的DNS又向linux178.com这个域名的DNS地址(这个一般就是由域名注册商提供的,像万网,新网等)发起请求(请问www.linux178.com这个域名的IP地址是多少?),这个时候linux178.com域的DNS服务器一查,诶,果真在我这里,于是就把找到的结果发送给运营商的DNS服务器,这个时候运营商的DNS服务器就拿到了www.linux178.com这个域名对应的IP地址,并返回给Windows系统内核,内核又把结果返回给浏览器,终于浏览器拿到了www.linux178.com对应的IP地址,该进行一步的动作了。 注:一般情况下是不会进行以下步骤的 如果经过以上的4个步骤,还没有解析成功,那么会进行如下步骤: 5. 操作系统就会查找NetBIOS name Cache(NetBIOS名称缓存,就存在客户端电脑中的),那这个缓存有什么东西呢?凡是最近一段时间内和我成功通讯的计算机的计算机名和Ip地址,就都会存在这个缓存里面。什么情况下该步能解析成功呢?就是该名称正好是几分钟前和我成功通信过,那么这一步就可以成功解析。 6. 如果第5步也没有成功,那会查询WINS 服务器(是NETBIOS名称和IP地址对应的服务器) 7. 如果第6步也没有查询成功,那么客户端就要进行广播查找 8. 如果第7步也没有成功,那么客户端就读取LMHOSTS文件(和HOSTS文件同一个目录下,写法也一样) 如果第八步还没有解析成功,那么就宣告这次解析失败,那就无法跟目标计算机进行通信。只要这八步中有一步可以解析成功,那就可以成功和目标计算机进行通信。
DNS也是开销,通常浏览器查找一个给定域名的IP地址要花费20~120毫秒,在完成域名解析之前,浏览器不能从服务器加载到任何东西。那么如何减少域名解析时间,加快页面加载速度呢?
当客户端DNS缓存(浏览器和操作系统)缓存为空时,DNS查找的数量与要加载的Web页面中唯一主机名的数量相同,包括页面URL、脚本、样式表、图片、Flash对象等的主机名。减少主机名的 数量就可以减少DNS查找的数量。
减少唯一主机名的数量会潜在减少页面中并行下载的数量(HTTP 1.1规范建议从每个主机名并行下载两个组件,但实际上可以多个),这样减少主机名和并行下载的方案会产生矛盾,需要大家自己权衡。建议将组件放到至少两个但不多于4个主机名下,减少DNS查找的同时也允许高度并行下载。
十、精简JavaScript
精简
精简就是从代码中移除不必要的字符以减少文件大小,降低加载的时间。代码精简的时候会移除不必要的空白字符(空格,换行、制表符),这样整个文件的大小就变小了。
混淆
混淆是应用在源代码上的另外一种方式,它会移除注释和空白符,同时它还会改写代码。在混淆的时候,函数和变量名将会被转换成更短的字符串,这时代码会更加精炼同时难以阅读。通常这样做是为了增加对代码进行反向工程的难度,这也同时提高了性能。
缺点:
混淆本身比较复杂,可能会引入错误。
需要对不能改变的符号做标记,防止JavaScript符号(譬如关键字、保留字)被修改。
混淆会使代码难以阅读,这使得在产品环境中调试问题更加困难。
在以上提到了关于用gzip之类的压缩方式来压缩文件,这边说明一下,就算使用gzip等方式来压缩文件,精简代码依然是有必要的。一般来说,压缩产生的节省是高于精简的,在生产环境中,精简和压缩同时使用能够最大限度的获得更多的节省。
CSS的精简
CSS的精简带来的节省一般来说是小于JavaScript精简的,因为CSS中注释和空白相对较少。
除了移除空白、注释之外,CSS可以通过优化来获得更多的节省:
合并相同的类;
移除不使用的类;
使用缩写,譬如
.right {
color: #fff;
padding-top: 0;
margin: 0 10px;
border: 1px solid #111
}
.wrong {
color: #ffffff;
padding-top: 0px;
margin-top: 0;
margin-bottom: 0;
margin-left: 10px;
margin-right: 10px;
border-color: #111;
border-width: 1px;
border-style: solid;
}
上面.right是正确的的写法,颜色使用缩写,使用0代替0px,合并可以合并的样式。另外,在精简的时候其实样式最后一行的';'也是可以省略的。
来看看精简的例子:
以上分别是jquery-2.0.3的学习版(未精简)和精简版,可见精简文件的大小比源文件小了155k,而且,在精简版中jquery还做了混淆,譬如用e代替window等,从而获得最大的节省。
十一、避免重定向
什么是重定向?
重定向用于将用户从一个URL重新路由到另一个URL。
常用重定向的类型
301:永久重定向,主要用于当网站的域名发生变更之后,告诉搜索引擎域名已经变更了,应该把旧域名的的数据和链接数转移到新域名下,从而不会让网站的排名因域名变更而受到影响。
302:临时重定向,主要实现post请求后告知浏览器转移到新的URL。
304:Not Modified,主要用于当浏览器在其缓存中保留了组件的一个副本,同时组件已经过期了,这是浏览器就会生成一个条件GET请求,如果服务器的组件并没有修改过,则会返回304状态码,同时不携带主体,告知浏览器可以重用这个副本,减少响应大小。
重定向如何损伤性能?
当页面发生了重定向,就会延迟整个HTML文档的传输。在HTML文档到达之前,页面中不会呈现任何东西,也没有任何组件会被下载。
来看一个实际例子:对于ASP.NET webform开发来说,对于新手很容易犯一个错误,就是把页面的连接写成服务器控件后台代码里,例如用一个Button控件,在它的后台click事件中写上:Response.Redirect("");然而这个Button的作用只是转移URL,这是非常低效的做法,因为点击Button后,先发送一个Post请求给服务器,服务器处理Response.Redirect("")后就发送一个302响应给浏览器,浏览器再根据响应的URL发送GET请求。正确的做法应该是在html页面直接使用a标签做链接,这样就避免了多余的post和重定向。
重定向的应用场景
1. 跟踪内部流量
重定向经常用于跟踪用户流量的方向,当拥有一个门户主页的时候,同时想对用户离开主页后的流量进行跟踪,这时可以使用重定向。例如: 某网站主页新闻的链接地址http://a.com/r/news,点击该链接将产生301响应,其Location被设置为http://news.a.com。通过分析a.com的web服务器日志可以得知人们离开首页之后的去向。
我们知道重定向是如何损伤性能的,为了实现更好的效率,可以使用Referer日志来跟踪内部流量去向。每个HTTP请求都有一个Referer表示原始请求页(除了从书签打开或直接键入URL等操作),记录下每个请求的Referer,就避免了向用户发送重定向,从而改善了响应时间。
2. 跟踪出站流量
有时链接可能将用户带离你的网站,在这种情况下,使用Referer就不太现实了。
同样也可以使用重定向来解决跟踪出站流量问题。以百度搜索为例,百度通过将每个链接包装到一个302重定向来解决跟踪的问题,例如搜索关键字“前端性能优化”,搜索结果中的一个URL为https://www.baidu.com/link?url=pDjwTfa0IAf_FRBNlw1qLDtQ27YBujWp9jPN4q0QSJdNtGtDBK3ja3jyyN2CgxR5aTAywG4SI6V1NypkSyLISWjiFuFQDinhpVn4QE-uLGG&wd=&eqid=9c02bd21001c69170000000556ece297,即使搜索结果并没有变,但这个字符串是动态改变的,暂时还不知道这里起到怎样的作用?(个人感觉:字符串中包含了待访问的网址,点击之后会产生302重定向,将页面转到目标页面(待修改,求大神们给我指正))
除了重定向外,我们还可以选择使用信标(beacon)——一个HTTP请求,其URL中包含有跟踪信息。跟踪信息可以从信标Web服务器的访问日记中提取出来,信标通常是一个1px*1px的透明图片,不过204响应更优秀,因为它更小,从来不被缓存,而且绝不会改变浏览器的状态。
十二、删除重复脚本
在团队开发一个项目时,由于不同开发者之间都可能会向页面中添加页面或组件,因此可能相同的脚本会被添加多次。
重复的脚本会造成不必要的HTTP请求(如果没有缓存该脚本的话),并且执行多余的JavaScript浪费时间,还有可能造成错误。
如何避免重复脚本呢?
1. 形成良好的脚本组织。重复脚本有可能出现在不同的脚本包含同一段脚本的情况,有些是必要的,但有些却不是必要的,所以需要对脚本进行一个良好的组织。
2. 实现脚本管理器模块。
例如:
1 function insertScript($file) {
2 if(hadInserted($file)) {
3 return;
4 }
5 exeInsert($file);
6
7 if(hasDependencies($file)) {
8
9 $deps = getDependencies($file);
10
11 foreach ($deps as $script) {
12 insertScript($script);
13 }
14
15 echo "<script type='text/javascript' src='".getVersion($file)."'></script>";
16
17 }
18 }
先检查是否插入过,如果插入过则返回。如果该脚本依赖其它脚本,则被依赖的脚本也会被插入。最后脚本被传送到页面,getVersion会检查脚本并返回追加了对应版本号的文件名,这样如果脚本的版本变化了,那么以前浏览器缓存的就会失效。
十三、配置ETag
以前浏览器缓存的就会失效。
什么是ETag?
实体标签(EntityTag)是唯一标识了一个组件的一个特定版本的字符串,是web服务器用于确认缓存组件的有效性的一种机制,通常可以使用组件的某些属性来构造它。
条件GET请求
如果组件过期了,浏览器在重用它之前必须首先检查它是否有效。浏览器将发送一个条件GET请求到服务器,服务器判断缓存还有效,则发送一个304响应,告诉浏览器可以重用缓存组件。
那么服务器是根据什么判断缓存是否还有效呢?有两种方式:
ETag(实体标签);
最新修改日期;
最新修改日期
原始服务器通过Last-Modified响应头来返回组件的最新修改日期。
举个栗子:
当我们不带缓存访问www.google.com.hk的时候,我们需要下载google的logo,这时会发送这样一个HTTP请求:
Request:
GET googlelogo_color_272x92dp.png HTTP 1.1
Host: www.google.com.hk
Response:
HTTP 1.1 200 OK
Last-Modified:Fri, 04 Sep 2015 22:33:08 GMT
当需要再次访问相同组件的时候,同时缓存已经过期,浏览器会发送如下条件GET请求:
Request:
GET googlelogo_color_272x92dp.png HTTP 1.1
If-Modified-Since:Fri, 04 Sep 2015 22:33:08 GMT
Host: www.google.com.hk
Response:
HTTP 1.1 304 Not Modified
实体标签
ETag提供了另外一种方式,用于检测浏览器缓存中的组件与原始服务器上的组件是否匹配。摘抄自书上的例子:
不带缓存的请求:
Request:
GET /i/yahoo/gif HTTP 1.1
Host: us.yimg.com
Response:
HTTP 1.1 200 OK
Last-Modified:Tue,12 Dec 200603:03:59 GMT
ETag:”10c24bc-4ab-457elc1f“
再次请求相同组件:
Request:
GET /i/yahoo/gif HTTP 1.1
Host: us.yimg.com
If-Modified-Since:Tue,12 Dec 200603:03:59 GMT
If-None-Match:”10c24bc-4ab-457elc1f“
Response:
HTTP 1.1 304 Not Midified
为什么要引入ETag?
ETag主要是为了解决Last-Modified无法解决的一些问题:
1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
3. 某些服务器不能精确的得到文件的最后修改时间。
ETag带来的问题
ETag的问题在于通常使用某些属性来构造它,有些属性对于特定的部署了网站的服务器来说是唯一的。当使用集群服务器的时候,浏览器从一台服务器上获取了原始组件,之后又向另外一台不同的服务器发起条件GET请求,ETag就会出现不匹配的状况。例如:使用inode-size-timestamp来生成ETag,文件系统使用inode存储文件类型、所有者、组和访问模式等信息,在多台服务器上,就算文件大小、权限、时间戳等都相同,inode也是不同的。
最佳实践
1. 如果使用Last-Modified不会出现任何问题,可以直接移除ETag,google的搜索首页则没有使用ETag。
2. 确定要使用ETag,在配置ETag的值的时候,移除可能影响到组件集群服务器验证的属性,例如使用size-timestamp来生成时间戳。
十四、使Ajax可缓存
维基百科中这样定义Ajax:
AJAX即“Asynchronous JavaScript and XML”(异步的JavaScript与XML技术),指的是一套综合了多项技术的浏览器端网页开发技术。Ajax的概念由杰西·詹姆士·贾瑞特所提出。
传统的Web应用允许用户端填写表单(form),当提交表单时就向Web服务器发送一个请求。服务器接收并处理传来的表单,然后送回一个新的网页,但这个做法浪费了许多带宽,因为在前后两个页面中的大部分HTML码往往是相同的。由于每次应用的沟通都需要向服务器发送请求,应用的回应时间依赖于服务器的回应时间。这导致了用户界面的回应比本机应用慢得多。
与此不同,AJAX应用可以仅向服务器发送并取回必须的数据,并在客户端采用JavaScript处理来自服务器的回应。因为在服务器和浏览器之间交换的数据大量减少(大约只有原来的5%)[来源请求],服务器回应更快了。同时,很多的处理工作可以在发出请求的客户端机器上完成,因此Web服务器的负荷也减少了。
类似于DHTML或LAMP,AJAX不是指一种单一的技术,而是有机地利用了一系列相关的技术。虽然其名称包含XML,但实际上数据格式可以由JSON代替,进一步减少数据量,形成所谓的AJAJ。而客户端与服务器也并不需要异步。一些基于AJAX的“派生/合成”式(derivative/composite)的技术也正在出现,如AFLAX。
Ajax的目地是为突破web本质的开始—停止交互方式,向用户显示一个白屏后重绘整个页面不是一种好的用户体验。
异步与即时
Ajax的一个明显的有点就是向用户提供了即时反馈,因为它异步的从后端web服务器请求信息。
用户是否需要等待的关键因素在于Ajax请求是被动的还是主动的。被动请求是为了将来来使用而预先发起的,主动请求是基于用户当前的操作而发起的
什么样的AJAX请求可以被缓存?
POST的请求,是不可以在客户端缓存的,每次请求都需要发送给服务器进行处理,每次都会返回状态码200。(可以在服务器端对数据进行缓存,以便提高处理速度)
GET的请求,是可以(而且默认)在客户端进行缓存的,除非指定了不同的地址,否则同一个地址的AJAX请求,不会重复在服务器执行,而是返回304。
Ajax请求使用缓存
在进行Ajax请求的时候,可以选择尽量使用get方法,这样可以使用客户端的缓存,提高请求速度。
vue性能能优化
template
语义化标签,避免乱嵌套,合理命名属性等等标准推荐的东西就不谈了。
模板部分帮助我们展示结构化数据,vue 通过数据驱动视图,主要注意一下几点
- v-show,v-if 用哪个?在我来看要分两个维度去思考问题,第一个维度是权限问题,只要涉及到权限相关的展示无疑要用
v-if
,第二个维度在没有权限限制下根据用户点击的频次选择,频繁切换的使用v-show
,不频繁切换的使用v-if
,这里要说的优化点在于减少页面中 dom 总数,我比较倾向于使用v-if
,因为减少了 dom 数量,加快首屏渲染,至于性能方面我感觉肉眼看不出来切换的渲染过程,也不会影响用户的体验。 - 不要在模板里面写过多的表达式与判断
v-if="isShow && isAdmin && (a || b)"
,这种表达式虽说可以识别,但是不是长久之计,当看着不舒服时,适当的写到 methods 和 computed 里面封装成一个方法,这样的好处是方便我们在多处判断相同的表达式,其他权限相同的元素再判断展示的时候调用同一个方法即可。 - 循环调用子组件时添加 key,key 可以唯一标识一个循环个体,可以使用例如
item.id
作为 key,假如数组数据是这样的['a' , 'b', 'c', 'a']
,使用:key="item"
显然没有意义,更好的办法就是在循环的时候(item, index) in arr
,然后:key="index"
来确保 key 的唯一性。
style
- 将样式文件放在 vue 文件内还是外?讨论起来没有意义,重点是按模块划分,我的习惯是放在 vue 文件内部,方便写代码是在同一个文件里跳转上下对照,无论内外建议加上
<style scoped>
将样式文件锁住,目的很简单,再好用的标准也避免不了多人开发的麻烦,约定命名规则也可能会冲突,锁定区域后尽量采用简短的命名规则,不需要.header-title__text
之类的 class,直接.title
搞定。 - 为了和上一条作区分,说下全局的样式文件,全局的样式文件,尽量抽象化,既然不在每一个组件里重复写,就尽量通用,这部分抽象做的越好说明你的样式文件体积越小,复用率越高。建议将复写组件库如 Element 样式的代码也放到全局中去。
- 不使用 float 布局,之前看到很多人封装了
.fl -- float: left
到全局文件里去,然后又要.clear
,现在的浏览器还不至于弱到非要用float
去兼容,完全可以 flex,grid 兼容性一般,功能其实 flex 布局都可以实现,float 会带来布局上的麻烦,用过的都知道不相信解释坑了。
至于其他通用的规范这里不赘述,相关文章很多。
script
这部分也是最难优化的点,说下个人意见吧。
- 多人开发时尽量保持每个组件
export default {}
内的方法顺序一致,方便查找对应的方法。我个人习惯 data、props、钩子、watch、computed、components。 - data 里要说的就是初始化数据的结构尽量详细,命名清晰,简单易懂,避免无用的变量,isEditing 实际可以代表两个状态,true 或 false,不要再去定义 notEditing 来控制展示,完全可以在模板里
{{ isEditing ? 编辑中 : 保存 }}
- props 父子组件传值时尽量
:width="" :heigth=""
不要:option={}
,细化的好处是只传需要修改的参数,在子组件 props 里加数据类型,是否必传,以及默认值,便于排查错误,让传值更严谨。 - 钩子理解好生命周期的含义就好,什么时间应该请求,什么时间注销方法,哪些方法需要注销。简单易懂,官网都有写。
- metheds 中每一个方法一定要简单,只做一件事,尽量封装可复用的简短的方法,参数不易过多。如果十分依赖 lodash 开发,methed 看着会简洁许多,代价就是整体的 bundle 体积会大,假如项目仅仅用到小部分方法可以局部引入 loadsh,不想用 lodash 的话可以自己封装一个 util.js 文件
- watch 和 computed 用哪个的问题看官网的例子,计算属性主要是做一层 filter 转换,切忌加一些调用方法进去,watch 的作用就是监听数据变化去改变数据或触发事件如
this.$store.dispatch('update', { ... })
组件优化
vue 的组件化深受大家喜爱,到底组件拆到什么程度算是合理,还要因项目大小而异,小型项目可以简单几个组件搞定,甚至不用 vuex,axios 等等,如果规模较大就要细分组件,越细越好,包括布局的封装,按钮,表单,提示框,轮播等,推荐看下 Element 组件库的代码,没时间写这么详细可以直接用 Element 库,分几点进行优化
- 组件有明确含义,只处理类似的业务。复用性越高越好,配置性越强越好。
- 自己封装组件还是遵循配置 props 细化的规则。
- 组件分类,我习惯性的按照三类划分,page、page-item 和 layout,page 是路由控制的部分,page-item 属于 page 里各个布局块如 banner、side 等等,layout 里放置多个页面至少出现两次的组件,如 icon, scrollTop 等
vue-router 和 vuex 优化
vue-router 除了切换路由,用的最多的是处理权限的逻辑,关于权限的控制这里不赘述,相关 demo 和文章有许多,那么说到优化,值得一提的就是组件懒加载
中文官网链接如上,例子如下
const Foo = r => require.ensure([], () => r(require('./Foo.vue')), 'group-foo')
const Bar = r => require.ensure([], () => r(require('./Bar.vue')), 'group-foo')
const Baz = r => require.ensure([], () => r(require('./Baz.vue')), 'group-foo')
这段代码将 Foo, Bar, Baz 三个组件打包进了名为 group-foo
的 chunk 文件,当然啦是 js 文件
其余部分正常写就可以,在网站加载时会自动解析需要加载哪个 chunk,虽然分别打包的总体积会变大,但是单看请求首屏速度的话,快了好多。
vuex 面临的问题和解决方案有几点
- 当网站足够大时,一个状态树下,根的部分字段繁多,解决这个问题就要模块化 vuex,官网提供了模块化方案,允许我们在初始化 vuex 的时候配置 modules。每一个 module 里面又分别包含 state 、action 等,看似是多个状态树,其实还是基于 rootState 的子树。细分后整个 state 结构就清晰了,管理起来也方便许多。
- 由于 vuex 的灵活性,带来了编码不统一的情况,完整的闭环是 store.dispatch('action') -> action -> commit -> mutation -> getter -> computed,实际上中间的环节有的可以省略,因为 API 文档提供了以下几个方法 mapState、mapGetters、mapActions、mapMutations,然后在组件里可以直接调取任何一步,还是项目小想怎么调用都可以,项目大的时候,就要考虑 vuex 使用的统一性,我的建议是不论多简单的流程都跑完整个闭环,形成代码的统一,方便后期管理,在我的组件里只允许出现 dispatch 和 mapGetters,其余的流程都在名为 store 的 vuex 文件夹里进行。
- 基于上面一条,说下每个过程里面要做什么,前后端数据一定会有不一致的地方,或是数据结构,或是字段命名,那么究竟应该在哪一步处理数据转换的逻辑呢?有人会说其实哪一步都可以实现,其实不然,我的建议如下
- 在发 dispatch 之前就处理好组件内需要传的参数的数据结构和字段名
- 到了 action 允许我们做的事情很多,因为这部支持异步,支持 state, rootState, commit, dispatch, getters,由此可见责任重大,首先如果后端需要部分其他 module 里面的数据,要通过 rootState 取值再整合到原有数据上,下一步发出请求,建议(async await + axios),拿到数据后进行筛选转换,再发送 commit 到 mutation
- 这一步是将转换后的数据更新到 state 里,可能会有数据分发的过程(传进一个 object 改变多个 state 中 key 的 value),可以转换数据结构,但是尽量不做字段转换,在上一步做
- 此时的 store 已经更新,使用 getter 方法来取值,
token: state => state.token
,单单的取值,尽量不要做数据转换,需要转换的点在于多个地方用相同的字段,但是结构不同的情况(很少出现)。 - 在组件里用 mapGetters 拿到对应的 getter 值。
打包优化
上面说了代码方面的规范和优化,下面说下重点的打包优化,前段时间打包的 vender bundle 足足 1.4M,app bundle 也有 270K,app bundle 可以通过组件懒加载解决,vender 包该怎么解决?
有人会质疑是不是没压缩或依赖包没去重,其实都做了就是看到的 1.4M。
解决方法很简单,打包 vender 时不打包 vue、vuex、vue-router、axios 等,换用国内的 bootcdn 直接引入到根目录的 index.html 中。
例如:
<script src="//cdn.bootcss.com/vue/2.2.5/vue.min.js"></script>
<script src="//cdn.bootcss.com/vue-router/2.3.0/vue-router.min.js"></script>
<script src="//cdn.bootcss.com/vuex/2.2.1/vuex.min.js"></script>
<script src="//cdn.bootcss.com/axios/0.15.3/axios.min.js"></script>
在 webpack 里有个 externals,可以忽略不需要打包的库
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'axios': 'axios'
}
此时的 vender 包会非常小,如果不够小还可以拆分其他的库,此时增加了请求的数量,但是远比加载一个 1.4M 的 bundle 快的多。
react 性能优化
一,react组件的性能优化(渲染角度优化)
1,react性能查看工具
再讲性能优化之前,我们需要先来了解一下如何查看react加载组件时所耗费的时间的工具,在react 16版本之前我们可以使用React Perf
来查看。
大家可以在chorme中先安装React Perf扩展,然后在入口文件或者redux
的store.js
中加入相应的代码即可:
React Perf
在最新的React16版本中,我们可以直接在url后加上?react_pref
,就可以在chrome浏览器的performance
,我们可以查看User Timeing
来查看组件的加载时间。
react16.0_pref
使用此工具的具体操作大家可以看下图:
react16.0_pref.gif
2,单个react组件性能优化
2.1,render
里面尽量减少新建变量和bind
函数,传递参数是尽量减少传递参数的数量。
首先我们先思考一个问题,比如我要实现一个点击按钮使相应的num
增加1,我们有哪一些方法。
大家应该都能想到,无非就是三种,如下图:
react_function
第一种是在构造函数中绑定this
,第二种是在render()
函数里面绑定this
,第三种就是使用箭头函数,都能实现上述方法;
但是哪一种方法的性能最好,是我们要考虑的问题。应该大家都知道答案:第一种的性能最好。
因为第一种,构造函数每一次渲染的时候只会执行一遍;
而第二种方法,在每次render()
的时候都会重新执行一遍函数;
第三种方法的话,每一次render()
的时候,都会生成一个新的箭头函数,即使两个箭头函数的内容是一样的。
第三种方法我们可以举一个例子,因为react
判断是否需要进行render
是浅层比较,简单来说就是通过===
来判断的,如果state
或者prop
的类型是字符串或者数字,只要值相同,那么浅层比较就会认为其相同;
但是如果前者的类型是复杂的对象的时候,我们知道对象是引用类型,浅层比较只会认为这两个prop
是不是同一个引用,如果不是,哪怕这两个对象中的内容完全一样,也会被认为是两个不同的prop
。
举个例子:
当我们给组件Foo
给名为style
的prop
赋值;
<Foo style={{ color:"red" }}
使用这种方法,每一次渲染都会被认为是一个style
这个prop
发生了变化,因为每一次都会产生一个对象给style
。
那么我们应该如何改进,如果想要让react
渲染的时候认为前后对象类型prop
相同,则必须要保证prop
指向同一个javascript
对象,如下:
const fooStyle = { color: "red" }; //取保这个初始化只执行一次,不要放在render中,可以放在构造函数中
<Foo style={fooStyle} />
这个问题是我们在平时的编码中可以避免的。
2.2,定制shouldComponentUpdate
函数
shouldComponentUpdate
是决定react
组件什么时候能够不重新渲染的函数,但是这个函数默认的实现方式就是简单的返回一个true
。也就是说,默认每次更新的时候都要调用所用的生命周期函数,包括render
函数,重新渲染。
我们来看一下下面的一个例子
shouldComponentUpdate
我们写两个组件,App
和Demo
组件,并写两个方法,一个改变App
中的num
的值,一个是改变title
,我们在Demo的render中打印render函数。我们可以看到以下的效果:
shouldComponentUpdate_demo
我们可以清晰的看到虽然demo
组件里的title
值没有改变,但是还是render
了。
为了解决这个问题,我们可以对demo组件进行如下的修改:
shouldComponentUpdate
只有当demo的title值发生改变的时候,我们才去render,我们可以看一下效果:
shouldComponentUpdate_demo
以上只是一个特别简单的一个对于shouldComponentUpdate
的定制。
在最新的react
中,react给我们提供了React.PureComponent
,官方也在早期提供了名为react-addons-pure-render-mixin
插件来重新实现shouldComponentUpdate
生命周期方法。
PureComponent
通过上述的方法的效果也是和我们定制shouldComponentUpdate
的效果是一致的。
但是我们要注意的是,这里的PureRender
是浅比较的,因为深比较的场景是相当昂贵的。所以我们要注意我们在1.1
中说到的一些注意点:不要直接为props设置对象或者数组、不要将方法直接绑定在元素上,因为其实函数也是对象
2.3,Immutable.js
先配上一张经典的图和经典的一句话:
Immutable
Shared mutable state is the root of all evil(共享的可变状态是万恶之源)
-- Pete Hunt
javascript
中的对象一般都是可变的,因为使用了引用赋值,新的对象简单的引用了原始对象,改变新对象将影响到原始对象。
举个例子:
foo = { a : 1 };
bar = foo;
bar.a = 2;
当我们给bar.a
赋值后,会发现foo.a
也变成了2,虽然我们可以通过深拷贝与浅拷贝解决这个问题,但是这样做非常的昂贵,对cpu
和内存会造成浪费。
这里就需要用到Immutable
,通过Immutable
创建的Immutable Data
一旦被创建,就不能再更改。对Immutable
对象进行修改、添加或删除操作,都会返回一个新的Immutable
对象。
这里我们将一下其中三个比较重要的数据结构
- Map:键值对集合,对应
Object
,Es6
种也有专门的Map
对象 - List:有序可重复列表,对应于
Array
- ArraySet:有序且不可重复的列表
我们可以看两个例子:
使用Map
生成一个immutable
对象
import { Map , is } from 'immutable';
let obj = Map({
'name': 'react study',
'course': Map({name: 'react+redux'})
})
let obj1 = obj.set('name','darrell');
console.log(obj.get('course') === obj1.get('course')); // 返回true
console.log(obj === obj1); // 返回false
Immutable.is
比较的是两个对象的 hashCode
或 valueOf
(对于 JavaScript 对象)。由于 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode
相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。
let obj = Map({name:1,title:'react'});
let obj1 = Map({name:1,title:'react'});
console.log(is(obj,obj1)); // 返回true
let obj2 = {name:1,title:'react'};
let obj3 = {name:1,title:'react'};
console.log(is(obj2,obj3)); // 返回false
Immutable
优点:
- 减少内存的使用
- 并发安全
- 降低项目的复杂度
- 便于比较复杂数据,定制shouldComponentUpdate方便
- 时间旅行功能
- 函数式编程
Immutable
缺点:
- 学习成本
- 库的大小(建议使用seamless-immutable)
- 对现有项目入侵严重
- 容易与原生的对象进行混淆
如果大家想深入了解,可以参考immutable、IMMUTABLE 详解。
2.4,多个react组件性能优化,key的优化
react
组件在装载过程中,react
通过在render
方法在内存中产生一个树形结构,树上的节点代表一个react
组件或者原生的Dom
元素,这个树形结构就是我们所谓的Vitural Dom
,react根据这个来渲染产生浏览器的Dom
树。
react
在更新阶段对比原有的Vitural Dom
和新生成的Vitural Dom
,找出不同之处,在根据不同来渲染Dom树。
react为了追求高性能,采用了时间复杂度为O(N)
来比较两个属性结构的区别,因为要确切比较两个树形结构,需要通过O(N^3)
,这会降低性能。
我们举几个情况,大家就会马上理解:
-
节点类型不同
// A组件 <div> <Todos /> </div> // B组件 <span> <Todos /> </span>
我们想把
A
组件更新成B
组件,react
在做比较的时候,发现最外面的根结点不一样,直接就废掉了之前的<div>
节点,包括里面的子节点,这是一个巨大的浪费,但是为了避免O(N^3)
的时间复杂度,只能采用这种方式所以在开发过程中,我们应该尽量避免上面的情况,不要将包裹节点的类型随意改变。
-
两个节点类型一样
这里包括两种情况,一种是节点是
Dom
类型,还有一种react
组件。对于
dom
类型,我们举个例子:// A组件 <div style={{color: 'red',fontSize:15}} className="welcome"> Hello World!!! </div> // B组件 <div style={{color: 'green',fontSize:15}} className="react"> Good Bye!!! </div>
上述A和B组件的区别是文字、
className
、style
中的color
发生改变,因为Dom
元素没变,React
只会修改他变化的部分。针对
react
组件类型,渲染无非就是在走一遍组件实例的更新过程,最主要的就是定制shouldComponentUpdate
,我们上面也有讲到,就不细讲了。 -
多个子组件情况
我们看两个例子就能明白
例子一:
// A <ul> <TodoItem text="First" complete={false} /> <TodoItem text="Second" complete={false} /> </ul> // B <ul> <TodoItem text="First" complete={false} /> <TodoItem text="Second" complete={false} /> <TodoItem text="Third" complete={false} /> </ul>
从A变到B,如果
shouldComponentUpdate
处理得当,我们只需要更新装载third
的那一次就行。我们来看看下一个例子:
// A <ul> <TodoItem text="First" complete={false} /> <TodoItem text="Second" complete={false} /> </ul> // B <ul> <TodoItem text="Zero" complete={false} /> <TodoItem text="First" complete={false} /> <TodoItem text="Second" complete={false} /> </ul>
这里因为react是采用O(n)的时间复杂度,所以会依次将text为First的改为Zero,text为Second改为First,在最后再加上一个组件,text为Second。现存的两个的text的属性都被改变了,所以会依次渲染。
如果我们这里有1000个实例,那么就会发生1000次更新。
这里我们就要用到
Key
了简单来说,其实这一个Key就是react组件的身份证号。
我们将上一个例子改成如下,就可以避免上面的问题了,react就能够知道其实B里面的第二个和第三个组件其实就是A中的第一个和第二个实例。
// A <ul> <TodoItem key={1} text="First" complete={false} /> <TodoItem key={2} text="Second" complete={false} /> </ul> // B <ul> <TodoItem key={0} text="Zero" complete={false} /> <TodoItem key={1} text="First" complete={false} /> <TodoItem key={2} text="Second" complete={false} /> </ul>
不过现在,react也会提醒我们不要忘记使用
key
,如果没有加,在浏览器中会报错。react_key
关于
key
的使用我们要注意的是,这个key值要稳定不变的,就如同身份证号之于我们是稳定不变的一样。一个常见的错误就是,拿数组的的下标值去当做key,这个是很危险的,代码如下,我们一定要避免。
<ul> { todos.map((item, index) => { <TodoItem key={index} text={item.text} completed={item.completed} }) } </ul>
二,redux性能优化:reselect(数据获取时优化)
在前面的优化过程中,我们都是优化渲染来提高性能的,既然react
和redux
都是通过数据驱动的的方式驱动渲染过程,那么处理优化渲染过程,获取数据的过程也是需要考虑的一个优化点。
//下面是redux中简单的一个筛选功能
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
mapStateToProps
函数作为redux store
中获取数据的重要一环,当我们根据filter
和todos
来显示相应的待办事项的时候,我们都要遍历todos
字段上的数组。
当数组比较大的时候,则会降低性能。
这个时候,reselect就应运而生了,它的动作原理:只要相关的状态没有改变,那么就直接使用上一次的缓存结果。
具体的用法我就不在这里过多介绍了,已经有很多的牛人写了相关的文章,我也不重复写了,大家如果想深入了解的话,可以参考reselect的giuhub、Redux的中间件-Reselect。
函数节流和防抖
函数节流(throttle)
-
指定时间间隔内只会执行一次任务
这里以判断页面是否滚动事件为例:
因为当在滚动的时候,浏览器会无时不刻地在计算判断是否滚动到底部的逻辑,而在实际的场景中是不需要这么做的,在实际场景中可能是这样的:在滚动过程中,每隔一段时间在去计算这个判断逻辑。而函数节流所做的工作就是每隔一段时间去执行一次原本需要无时不刻地在执行的函数,所以在滚动事件中引入函数的节流是一个非常好的实践:没有进行函数的节流
//utils.js
//函数节流
export function throttle(fn, waitTime = 1000){
let lastTime = null
return function () {
let startTime = + new Date()
if (startTime - lastTime > waitTime || !lastTime) {
fn()
lastTime = startTime
}
}
}
let throttle = (fn, delay = 50) => {
// 节流 控制执行间隔时间 防止频繁触发 scroll resize mousemove
let stattime = 0;
return function (...args) {
let curTime = new Date();
if (curTime - stattime >= delay) {
fn.apply(this, args);
stattime = curTime;
}
}
}
加入函数节流函数
使用方法
this.$refs.Throttle.addEventListener('scroll',
throttle(that.console, 300)
)
函数防抖(debounce)
-
任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。
没有加入防抖函数
很明显,这样的做法不好的是当用户输入第一个字符的时候,就开始请求判断了,不仅对服务器的压力增大了,对用户体验也未必比原来的好。而理想的做法应该是这样的,当用户输入第一个字符后的一段时间内如果还有字符输入的话,那就暂时不去请求判断用户名是否被占用。在这里引入函数防抖就能很好地解决这个问题
其实函数防抖的原理也非常地简单,通过闭包保存一个标记来保存 setTimeout 返回的值,每当用户输入的时候把前一个 setTimeout clear 掉,然后又创建一个新的 setTimeout,这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数了。
utils.js
//函数防抖
export function debounce(fn, interval = 300) {
let timer = null;
return function () {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, interval);
};
}
let debounce = (fn, time = 50) => { // 防抖动 控制空闲时间 用户输入频繁
let timer;
return function (...args) {
let that = this;
clearTimeout(timer);
timer = setTimeout(fn.bind(that, ...args), time);
}
}
加入防抖函数.gif
使用方法
原生的js:
$('input.user-name').on('input', debounce( () => {
//somecode
}, 800)
在vue中的使用:
//Debounce.vue
<input type="text" @input='bindInputDebounce' >
methods:{
bindInputDebounce:debounce(function() {
console.log('bindInputDebounce')
})
}
总结
- 使用函数节流与函数防抖的目的,在开头的栗子中应该也能看得出来,就是为了节约计算机资源。