煮酒论JavaScript:使用“更好的”代码(高性能的密码)

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

笔者相关文章分享(性能优化在实际中的使用):
https://blog.csdn.net/qq_43624878/article/details/95226831
https://blog.csdn.net/qq_43624878/article/details/97685279


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

我不是搞安全的,但是搞前端还是要多了解一下这方面内容,这里还是提一下 XSS攻击 吧:

  • 反射型
  • 存储型

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标签

笔者相关文章分享:前端XSS攻击后端密码安全问题防范两个“特别的”安全策略


正式开篇

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

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

可能见风向不好吧,浏览器迅速推出了两个解决方法:defer和async属性!


无阻塞脚本

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

  1. defer:延时加载(只能在IE中使用!)
  2. async:加载完成后自动执行——与defer的相同点是 并行加载(但是加载完即执行,时间不可控!)

动态脚本 比如:

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

现如今,这种动态脚本的方式几乎成了异步加载的标准:

function loadjs(script_filename) {
    var script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    script.setAttribute('src', script_filename);
    script.setAttribute('id', 'Mxc_id');

    script_id = document.getElementById('Mxc_id');
    if(script_id){
        document.getElementsByTagName('head')[0].removeChild(script_id);
    }
    document.getElementsByTagName('head')[0].appendChild(script);
}

//使用如:
var script = './js/Mxc.js';
loadjs(script);

甚至还衍生出了“各种姿势”——比如:按需加载之onload:

window.load=loadjs("./js/Mxc.js")
// 或点击事件中调用这个函数

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.min.js的包链接如下,直接下载即可:
https://pan.baidu.com/s/1N0NS740Xqm20iLg62yQiOg

「动态脚本」的好处在于:“它是在js渲染完成才执行的”,也就是说,如果在某些场景下而且你搭配合理的话,甚至在页面展示出来之前都不会牺牲时间去加载脚本。

但是上面的方法还是有些小“瑕疵”的:如果加载脚本需要一点时间,那么单线程的JavaScript是干不了任何事的( new Worker() 除外)。所以说,在笔者的“优先策略”中,用promise是最好的!
上面的代码是在简单的原生js中使用的,如果在支持import的项目中我们可以封装一下:

const importModule = url => {
  // 返回一个新的 Promise 实例
  return new Promise((resolve, reject) => {
    // 创建 script 标签
    const script = document.createElement("script");
    const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
    script.type = "module";
    script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;
    // load 回调
    script.onload = () => {
      resolve(window[tempGlobal]);
      delete window[tempGlobal];
      script.remove();
    };
    // error 回调
    script.onerror = () => {
      reject(new Error("Failed to load module script with URL " + url));
      delete window[tempGlobal];
      script.remove();
    };
    document.documentElement.appendChild(script);
  });
}

“小梦优先策略”:
HTML > CSS > JavaScript、
解决阻塞的最好方法一定是异步、
用户体验大于功能完美


数据存取

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

闭包
闭包是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要快。(后者也要比window.location.href.toString()要快!)

tip:
如果用到的属性不是对象的实例属性,那么成员解析还需要搜索原型链,会更慢!

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

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;
}

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

笔者相关文章推荐:Ajax请求优化新体验前端缓存的魅力

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事件的技术。它 基于 一个事实:事件冒泡 !我们只需给外层(父元素)绑定一个处理器,就可以处理器子元素上触发的所有事件。
这两天了解了H5的所谓“局部页面刷新”:不过是为了在不影响页面整体的情况下得到比如某一部分选项卡中第几个卡片上的值罢了。 但是在JS下,你想要拦截所有点击事件,并阻止其默认行为(打开链接)——只去发送一个Ajax请求来获取内容,然后局部更新页面。用事件委托来实现这个过程,你只需要给所有链接的外层元素添加一个点击监听器,让它捕获并分析点击是否来自链接:

document.getElementById('外层元素id').onclick=function(e){
	//浏览器target
	e=e || window.event;
	var target=e.target || e.srcElement;
	var pageid,hrefparts;
	
	//非链接即退出
	if(target.nodeName !== 'A'){
		return;
	}
	
	//从链接中找出页面id
	hrefparts=target.href.split('/');
	pageid=hrefparts[hrefparts.length-1];
	pageid=pageid.replace('.html','');
	
	//更新页面
	ajaxRequest('xhr.php?page='+pageid,updatePageContents);
	
	//浏览器阻止默认行为并取消冒泡
	if(typeof e.preventDefault === 'function'){
		e.preventDefault();
		e.stopPropagation();
	}else{
		e.returnValue=false;
		e.cancelBubble=true;
	}
};


结合前面所说的“最小化重绘和重排”,我想到了 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树中,文档片段被其所有的子元素代替。——把它(DocumentFragment)当做“父亲”!
因为文档片段不再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)

缓存

正如前面所说,JavaScript可用资源有限——这是JS与其它语言不同的地方。所以,在优化的道路上,出现了 Memoization

减少工作量就是最好的性能优化技术

顺着这个思路,我们当然可以想到:“避免重复工作” !代码要处理的事越少,它的速度也就越快 —— 缓存
这使他成了递归中有用的“算法”。
考虑如下代码:

function factorial(n){
	if(n==0){return 1;}
	else{return n*factorial(n-1);}
}

在代码运行过程中多次调用递归函数,大量重复工作无可避免:

var fact6=factorial(6);
var fact5=factorial(5);
var fact4=factorial(4);

这段代码有3次阶乘运算,导致factorial()函数被调用了18次。
而这段代码中最糟糕的部分在于:所有必要的计算都在第一行被处理!(6!=65!,5!=54!,4!=4*3!… 5!被计算了两次,而4!被计算量3次!)
你当然可以用 Memoization技术 来重写factorial()函数,它的宗旨在于:所有必要的计算在第一行进行缓存! 由于篇幅原因,我们直接来封装一个memoize()函数:

function memoize(fun,cache){
	cache=cache || {};
	var shell=function(arg){
		if(!cache.hasOwnProperty(arg)){
			cache[arg]=fun(arg);
		}
		return cache[arg];
	};
	return shell;
}

memoize()函数接收两个参数:一个是需要增加缓存功能的函数,一个事可选的缓存对象。
创建一个封装了原始函数(fun)的外壳(shell)函数,以确保只有当一个结果值之前从未被计算过时才会产生新的计算。随后这个外壳函数被返回,你可以直接调用它:

//缓存该阶乘函数
var memfactorial=memoize(factorial,{"0":1,"1":1});

//调用新函数
var fact6=memfactorial(6);
var fact5=memfactorial(5);
var fact4=memfactorial(4);

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。你不能设置超时处理/重试,事实上,就算失败了也不一定知道。笔者实在不推荐使用(其实我没用过…)


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

  1. 减少请求数。比如:合并JS和css文件(stylus or sass?)
  2. 缩短页面加载时间。可以在主要内容加载完后,再通过ajax获取次要内容
  3. 减少404响应!它的请求是昂贵的
  4. 原生JS编写底层代码
  5. 资源预加载
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

恪愚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值