高性能JavaScript:使用“更好的”代码

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_43624878/article/details/102694623

本文是一篇纯干货前端分享,也算是笔者最近一段时间的思考总结。(顺便为1024庆祝!)文章有些长,希望各位能坚持看完吧。
首先,你需要知道,这篇文章只是平日所用的总结,虽然涵盖大多数,但并不面面俱到,而且,我其实并不擅长写这种长文章,本文之外的更高级用法我会以短文的形式发博客。

前文相关分享:
https://blog.csdn.net/qq_43624878/article/details/95226831
https://blog.csdn.net/qq_43624878/article/details/97685279

引言-关于安全(放到最后有些突兀,想来想去,还是放到第一个位置吧)

我不是搞安全的,这里只是提一下 XSS攻击

  • 反射型
  • 存储型

反射型XSS攻击: XSS代码在URL中随输入提交(请求)到服务器端,服务器端解析后响应。XSS代码随响应内容一起回到浏览器,被执行。
这是一个明文攻击,或者,常表现为“诱导型攻击”。
存储型CSS攻击: 他和反射型攻击唯一的区别在于代码存储地方。存储型CSS,其提交代码会被存储在服务端(数据库、内存。文件系统…)

XSS防御

  • 编码 ——对用户输入的数据进行HTML Entity编码:'' - &quot;& - &amp;< - &lt;> - &gt;不断开空格 - &nbsp; (前面的是HTML内容,后面是编码成什么样子)
  • 过滤 —— 1、移除用户上传的DOM属性,如:onerror; 2、移除用户上传的style节点、script节点、Iframe节点、frame节点、link节点… ——比如这样:if(tag==’ … ’ || …) return;
  • 校正 —— 避免直接对HTML Entity编码,使用DOM Parse转换,校正不配对的DOM标签

正式开篇

对于JS文件,每个文件必须等到前一个文件下载并执行完才会开始下载。在这个过程中,用户看到的是一片空白(就是我们常说的“首屏空白”)。
现如今 浏览器基本实现了并行下载JS文件 ,但JS下载过程仍然会阻塞其他资源下载(比如图片)——页面需等待所有js代码下载并执行完才能继续。
现在很多人提倡的一种缓解办法是:把< script >标签放在body底部。这也是雅虎提出的JS优化首要规则。

  1. 不要把内嵌脚本紧跟在link标签之后 :实践发现,这样做会导致页面阻塞去等待样式表下载(浏览器觉得这样可确保内嵌脚本在执行时能获得精准的样式信息)
  2. 减少页面中外链脚本数量会大大改善性能 ——这和HTTP请求带来的(额外)性能开销有关:下载单个100KB文件远比下载4个25KB文件要快。
    (关于第二条,我们可以通过离线的打包工具或类似于Yahoo!combo handler实时在线服务实现。而且,前些年,淘宝也启用了combo handler,也开源了自己的包,还可以,不过淘宝的这个貌似需要后端(服务器)代码的支持,emmmmmmm)

无阻塞脚本

——实现 在window对象的load事件后再加载脚本 的效果
延迟脚本:

  1. defer:延时加载
  2. async:加载完成后自动执行——与defer的相同点是 并行加载

动态脚本 比如:

var script=document.createElement("script");
script.type="text/javascript";
script.src="某js文件路径";
document.getElementsByTagName("head")[0].appendChild(script);

XMLHttpRequest动态脚本注入
这个方法稍过繁琐,而且由于XMLHttpRequest的特性,遵从 同源策略 ,这就意味着JS文件不能从CDN下载,增加了请求负担。

更加推荐的方法:
LazyLoad类库——有Yahoo!的工程师创建的更nb的延迟加载工具。它是loadscript()的增强版!

<script type="text/javascript" src="lazyload-min.js"></script>
<script>
	LazyLoad.js("实例js文件(若是有多个,要以数组形式)",function(){
		Application.init();
	});
</script>

相关lazyload.js的包在下面,直接下载即可:
https://pan.baidu.com/s/1QmzFK4mNoDiIgx29rN7NDw

数据存取

标识符解析 :在执行环境的作用域链中,标识符所在位置越深,读写速度越慢。
因此,在函数中读写局部变量是最快的,而对全局变量是最慢的——全局变量总是存在于执行环境作用域链的最末端。
建议:如果某个跨作用域的值在函数中被引用一次以上,就把它存储到局部变量里!

闭包
闭包是JS里最强大的特性之一。它允许函数访问局部作用于之外的数据。
说起闭包,就要提一下 原型链 。这里分享一道“手写面试题”——实现一个简单的原型链:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>一个贴近实战的原型链实例 - 可面试时用</title>
</head>
<body>
<script>
	function Elem(id){
		this.elem=document.getElementById(id);
	}
	Elem.prototype.html=function(val){
		var elem=this.elem;
		if(val){
			elem.innerHTML=val;
		}else{
			return elem.innerHTML;
		}
	};
	Elem.prototype.on=function(type,fn){
		var elem=this.elem;
		elem.addEventListener(type,fn);
	};
	var div1=new Elem('div1');   //这个地方可以自行换成需要的节点
	div1.html('<p>hello mxc</p>').on('click',function(){
		alert('clicked');
	}).html('<p>mxc learned javascript</p>');
</script>
</body>
</html>
//以原型方式实现事件(比如本例的on(实现操作监听)、html(实现内容替换)),以链式聚合在一起,即为“原型链”

注意:解析原型链也要花不少时间 —— 执行location.href总是比window.location.href要快。

由上可知,几乎所有类似的性能问题都与对象成员有关。我们可以缓存一下,避免多次取同一个对象成员。比如:

function hasEnterClass(element,className1,calssName2){
	var currentClassName=element.className;
	return currentClassName==className1 || currentClassName==className2;
}

如上所说,JS数据存储的四个位置中,字面量局部变量 的访问速度快,应尽量减少对 数组元素对象成员 的访问次数。

“昂贵的”DOM

笔者的相关博客中一直在强调一件事:脚本操作DOM的代价是很昂贵的。它是富Web应用中最常见的性能瓶颈。
微软MIX09回忆中提到了一句话:“DOM天生就慢”,把DOM和JS(这里具体指ES)各自想象成一个岛,它们之间采用“收费桥梁”!访问DOM次数越多,费用也就越高。—— 雅虎性能优化小组推荐“ 尽可能地减少过桥次数 ”。
说起DOM,不得不提:DOM的修改。看下面两个两段代码:

function innerH(){
	for(var i=0;i<50000;i++){
		document.getElementById('here').innerHTML+='a';
	}
}
function innerH(){
	var content='';
	for(var i=0;i<50000;i++){
		content+='a';
	}
	document.getElementById('here').innerHTML+=content;
}

事实证明,在所有浏览器中第二个代码的性能比之第一个来说都是巨大提升! …缓存的魅力

innerHTML属性在除最新版Webkit内核浏览器中速度会更快一些 —— 相比标准的DOM方法(如document.createElement()和document.createTextNode()),所以,在一个对性能有苛刻要求的操作中更新一大段HTML,推荐使用innerHTML。

还有,访问集合时使用局部变量,这对性能的提升是“不可想象的”。

重绘和重排

首先要知道:重排一定会引起重绘! (反之却不一定)
什么时候发生重排?

  1. 添加/删除可见DOM元素
  2. 元素位置改变
  3. 元素尺寸改变(margin、padding、border、weight、width…)
  4. 内容改变
  5. 页面渲染器初始化
  6. 浏览器窗口尺寸改变

有些改变甚至会引起整个页面的重排:比如 滚动条出现时!
正因重排和重绘昂贵的代价,因此一个好的提高程序响应速度的策略就是减少此类操作的发生 —— 应该合并多次对DOM和样式的修改,然后一次处理掉。即 最小化重绘和重排 。(这和js设计模式中“利用虚拟代理合并http请求”的道理是差不多的)
看这个:

var el=document.getElementById('mydiv');
el.style.borderLeft='1px';
el.style.borderRight='2px';
el.style.padding='3px';

上面三个样式属性每一个都会影响到其几何结构。在最糟糕的情况下,会导致浏览器触发三次重排。(虽然现代浏览器基本都对此做了优化,但是,“永远不要相信浏览器”是一条不错的“定律”。。。)

要达到我们先要的效果。我想到了 CSSText属性

var el=document.getElementById('mydiv');
el.style.cssText='border-left:1px;border-right:2px;padding:5px';

若是不想“覆盖”原有样式,我们还可以这么写:el.style.cssText+='...';

让DOM脱离文档流/动画流

当你想要对DOM元素进行一系列操作时,你可以这么做:

  1. 使元素脱离文档流
  2. 对其进行多重改变
  3. 把元素带回文档中

在一些简单而又“复杂”的交互动画,比如:用展开/折叠的方式来显示/隐藏部分页面 中,我们依然可以如上面这般操作…

我们要尽量避免IE上的:hover

事实证明,在IE上使用hover伪元素是极不明智的决定,它会降低响应速度,过多地占用CPU,拉低性能(这在IE8中似乎更加明显)

事件委托

这是个不错的方案——针对绑定事件处理器(如:onclick)产生的代价(它要么加重了页面负担,要么增加了执行时间)
事件委托就是这么一个简洁而优雅的处理DOM事件的技术。它 基于 一个事实:事件冒泡 !我们只需给外层(父元素)绑定一个处理器,就可以处理器子元素上触发的所有事件。


结合前面所说的“最小化重绘和重排”,我想到了 DocumentFragments ,这里有两个例子:

var el,i=0,fragment=document.createDocumentFragment();
while(i<200){
	el=document.createElement("li");
	el.innerText='is number: '+i;
	fragment.appendChild(el);
	i++;
}
div.appendChild(fragment);

这样就只有一个(大的)DOM更改。哦,DocumentFragment是DOM节点,但不是主DOM树的一部分,通常我们用其创建文档片段,将元素附加到文档片段上,然后将文档片段加到DOM树。在DOM树中,文档片段被其所有的子元素代替。
因为文档片段不再DOM树中(在内存里),所以讲子元素插入到文档片段时不会引起页面回流!

它还可以当做“事件代理”来用:

let container = document.getElementById('container');
let wrapper = document.createDocumentFragment;   // 重要!!!
for(let i = 0; i < data.length; i++ ){
  let li = document.createElement('li');
  li.innerText = data[i];
}
wrapper.appendChild(li); 
container.appendChild(wrapper);
//给外层(父元素)绑定一个处理器
container.addEventListener('click', function(e){
  target = e.target || e.srcElement;
  if(target.tagName.toLowerCase() == 'li'){
    // 触发click后要做的事情
  }
}, false)

JS正则表达式

不得不说,当你小心使用正则时,它会非常高效。然而,当你只是搜索字面字符串时常常会弄巧成拙:当你 检查一个字符串是否以分号结尾

endWidth=/;$/.test(str);

浏览器会如此“智能”?
它们所做的只是检查每一个字符,找到每一个分号,然后确定下一个标记($),判断他是不是字符串的末尾。

基于此,我们何不跳过一切中间步骤,去检查“倒数第一个字符”呢?

endWidth=str.charAt(str.length-1)==";";

我们还可以 使用正则表达式去除首尾空白 ——这可以用到登录注册里面

if(!String.prototype.trim){
	String.prototype.trim=function(){
		return this.replace(/^\s+/,"").replace(/\s\s*$/,"");
	}
}
//测试
var str="\t\n test string".trim();
console.log(str=="test string");   // true

if中的作用是覆盖已存在的trim方法。因为原生的方法速度通常是最快的。

Ajax

提到JS比会说ajax —— 这个通过延迟下载体积较大的资源文件来使得页面加载的速度更快的措施实在好用,它通过异步方式在客户端和服务器之间传输数据,从而避免页面资源一窝蜂的下载。
现代高性能JS通常采用三种技术:XHR、动态脚本注入、multipart XHR 来向服务器请求数据。
有关XHR已多次讨论,这里不占篇幅。
动态脚本注入倒是需要注意,其无法设置“头信息”,请求方式也只能用GET,而非POST。你不能设置超时处理/重试,事实上,就算失败了也不一定知道。笔者实在不推荐使用(其实我没用过…)

字符格式

XML、json、HTML、TXT、自定义格式…
事实上,没有哪种格式永远是最好的。但通常来说的确是 越轻量级越好 ——JSON字符分隔的自定义格式 就是这样。
在这里插入图片描述
在这里插入图片描述

如果说数据集很大而且对解析时间又有要求,推荐使用如下两种方式:

  • JSON-P数据,使用动态脚本注人获取。它把数据当作可执行JavaScript 而不是字符串,解析速度极快。它能跨域使用,但涉及敏感数据时不应该使用它。
  • 字符分隔的自定义格式,使用XHR或动态脚本注入获取,用split()解析。这项技术解析大数据集比JSON-P略快,而且通常文件尺寸更小。

最后,笔者提供一些方法,有助于你的ajax:

  1. 减少请求数。比如:合并JS和css文件(sass?),或使用MXHR
  2. 缩短页面加载时间。可以在主要内容加载完后,通过ajax获取次要内容
  3. 减少404响应!它的请求是昂贵的
  4. 原生JS编写底层代码

猜你喜欢

转载自blog.csdn.net/qq_43624878/article/details/102694623