最近在阅读泽卡斯(NicholasC.Zakas)的,《高性能 JavaScript 》。为了加深印象,同时分享知识,以下是我个人阅读理解做的归纳以及笔记。
声明:该书是中阶进阶读的书,如果,你连JavaScript基础语法都不明了,也没必要深读下去。
第一章–>JavaScript 加载与执行
总结:
不喜欢看长篇大论的直接看下面的,内容总结。
- 在闭合标签之前,尽量将所有的<script> 标签放在页面底部。这能确保脚本执行之前页面已经渲染完。
- 合并脚本,减少 HTTP 请求,这样加载速度更快。
- 使用无阻塞下载JavaScript:
- 使用<script>标签的 defer 属性(有兼容问题)。
- 使用动态创建的<script>元素来下载执行代码。
- 使用XHR对象下载 JavaScript 代码并注入页面中。
问题:
js 在浏览器中阻塞的特性,可以认为是开发者所面临的最严重的可用性问题。在如今多数浏览器都是使用单一进程来处理用户的界面(UI)刷新 和 js 脚本执行。也就是说,同一时刻只能做一件事。js 执行过程耗时越久,浏览器等待响应的事件就越长。
//举个栗子:
<!doctype html>
<html>
<head>
<title>Document</title>
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.2.4.js"></script>
</head>
<body>
<p>123</p>
</body>
</html>
↑ 当浏览器遇到<script>标签时,当前 HTML 页面无法获知 js 是否会改变 HTML 结构,还是引入新元素, 或甚至关闭标签。因此,这时浏览器会停止处理页面,先执行 js 代码,然后在解析和渲染页面。
上面使用了国外的 JQ CDN,加载速度非常的慢,正常情况要 1-4s。这个时候,浏览器呈现一片空白。需要等待 JQ 下载完成,并执行完。才能接着处理页面。
<!doctype html>
<html>
<head>
<title>Document</title>
<script src="file.js1"></script>
<script src="file.js2"></script>
<script src="file.js3"></script>
</head>
<body>
<p>123</p>
</body>
</html>
↑ 上面看上去很正常的页面结构,实际上有十分严重的性能问题: 在<head>中加载三个<script>。
由于脚本会阻塞页面渲染,会直到它们全部下载完成并执行完成后。才会接着渲染页面。
↑ 上图可以看出,第一个 js 文件开始下载,与此同时也会阻塞了页面其他文件下载。此外,从 file.js1 下载完成到 file.js2 开始下载前存在一个延时,这个延时就是执行过程。每一个文件都要等到前一个文件下载并执行完成才会开始下载。这个期间,用户看到的是一片空白。
解决问题
有问题自然有解决方案,最为简单直接的方法是将<script>标签放在</body>之前。
<!doctype html>
<html>
<head>
<title>Document</title>
</head>
<body>
<p>123</p>
<script src="file.js1"></script>
<script src="file.js2"></script>
<script src="file.js3"></script>
</body>
</html>
↑ 将<script>标签放在</body>之前,这时候尽管<script>标签仍然阻塞另一个脚本,但是,页面的大部分内容已经下载完成并显示给了用户,这样显得页面不会加载太慢。这是雅虎特别性能小组提出的优化JavaScript的首要规则:将脚本放在底部。
无阻塞的脚本加载
上面的方法看上去简单容易执行。但是,脚本阻塞其他脚本。不能并行下载的问题依然存在。
下面介绍无阻塞加载<script>的方法。由于DOM的存在,你可以用 js 动态创建HTML中的几乎所有内容。而我们也能动态的创建<script>元素,来加载 js 。
var oScript = document.createElement("script"); //创建script元素
oScript.src = 'file.js1'; //增加url
document.getElementsByTagName("head")[0].appendChild(oScript); //插入元素
↑ 上面创建<script>元素,并添加url之后,添加到<head>元素中。这种技术的重点在于: 无论在何时启动下载,文件的下载和执行过程都不会阻塞页面其他进程。也就是说如果你添加多个<script>标签,这个下载过程也是并行的。但是,这样也会带来麻烦,看下面的例子。
//加入第一个 <script>
var oScript1 = document.createElement("script");
oScript1.src = 'file.js1';
document.getElementsByTagName("head")[0].appendChild(oScript1);
//加入第二个 <script>
var oScript2 = document.createElement("script");
oScript2.src = 'file.js2';
document.getElementsByTagName("head")[0].appendChild(oScript2);
↑ 上面加载了两个<script>,那么他们会并行下载 js 文件,下载完成后立即执行(除了 Firefox 和 Opera,他们会等待此前的所有动态脚本节点执行完毕)。脚本’自执行‘,这种机制运行正常。但是,如果 'file.js2' 需要调用 'file.js1' 的代码接口时,但是,'file.js1'还没有下载完毕,这时候,就会出现问题。所以,这种情况下需要跟踪并确保脚本下载完成准备就绪。
var oScript1 = document.createElement("script");
oScript1.onload = function(){
alert('file.js1加载完成');
}
oScript1.src = 'file.js1';
document.getElementsByTagName("head")[0].appendChild(oScript1);
↑ 上面利用 load 事件,来监听<script>元素是否加载完成。该事件只有 Firefox、Opera、Chorme、Safari 3以上的版本浏览器支持。你没看漏,IE不支持/(ㄒoㄒ)/~~。
IE支持另一种实现方式,它会触发一个 readystatechange 事件。<script> 元素提供一个 readyState 属性,它的的值随着外链文件的下载过程的不同阶段发生变化,该属性有五种取值:
- ‘uninitialized’ 初始状态
- ‘loading’ 开始下载
- ‘loaded’ 下载完成
- ‘interactive’ 数据完成下载当尚不可用
‘complete’ 所有数据已准备就
由于 IE 在标识最终状态 readyState 的值并不一致,有时到达“loaded”状态而不会到达“complete”,有时又不经过“loaded”,就到达“complete”。最好的方法是同时检查两种状态,只要其中之一任何一个触发了,就清除事件处理器(以确保事件不会处理两次)。
function loadScript(url, callBack) {
var oScript = document.createElement("script");
if (oScript.readyState) { //IE
oScript.onreadystatechange = function() {
if (oScript.readyState == "loaded" || oScript.readyState == "complete") {
oScript.onreadystatechange = "null"; //清空事件
callBack(); //回调函数
}
}
} else {// 非 IE
oScript.onload = function() {
callBack(); //回调函数
}
}
oScript.src = url;
document.getElementsByTagName("head")[0].appendChild(oScript);
};
↑ 结合前面的例子,写成一个函数,调用方法如下:
loadScript('file.js1',function(){ //先加载第一个
loadScript('file.js2', function(){ //回调加载第二个
...
})
});
↑ 这段代码会先加载第一个 file.js1,等待 file.js1 加载完成后,在加载 file2.js,以此类推。尽管方案可行,当如果下载的文件较多,可能会带来管理麻烦。
–>未完待续。