承接前面一篇文章《浏览器的渲染原理简介》 ,本文来说下JavaScript的装载和执行。
通常来说,浏览器对于 JavaScript 的运行有两大特性:
1) 载入后马上执行
2) 执行时会阻塞页面后续的内容(包括页面的渲染、其他资源的下载)
所以,如果有多个JS文件被引入,那么对于浏览器来说,这些JS文件将被串行地载入并依次执行。
由于JavaScript 可能会操作 HTML文档的DOM 树,所以浏览器一般都不会像并行下载CSS文件一样并行下载JS文件,这是JS文件的特殊性造成的。因此,如果你的JavaScript想操作后面的DOM 元素,浏览器会报错说找不到对象,这是因为JavaScript执行时后面的HTML被阻塞住了,操作DOM 树时还没有后面的节点。
传统方式
当你写下如下代码时:
<script type="text/javascript" src="http://coolshell.cn/asyncjs/alert.js"></script>
基本上来说,head里的<script>标签会阻塞后续资源的载入以及整个页面的生成。比如上面这个示例,其中只有一句JS代码(示例):
alert(“hello world”) ;
效果是在加载此JS文件时会弹出一个对话框,因此点击这个对话框后才会对后续资源进行载入以及对整个页面的进行生成。
所以,有很多网站会把 js 放在网页的最后面,或者使用 window.load、$(document).ready(function(){}) 之类的事件。
另外,由于绝大多数的JavaScript代码并不需要等待页面,我们需要异步载入功能。那我们怎么异步载入呢?
document.write 方式
你可能以为 document.write() 方式能够解决不阻塞的方式。通过 document.write 方法写入<script>标签的方式就可以执行后面的东西,对于在同一个 script 标签内的 JS代码来说是这样的。但是,对于整个页面来说,还是会阻塞的。下面是一段测试代码(示例):
<script type="text/javascript" language="javascript"> function loadjs(script_filename) { document.write('<' + 'script language="javascript" type="text/javascript"'); document.write(' src="' + script_filename + '">'); document.write('<'+'/script'+'>'); alert("loadjs() exit..."); } var script = 'http://coolshell.cn/asyncjs/alert.js'; loadjs(script); alert("loadjs() finished!"); </script> <script type="text/javascript" language="javascript"> alert("another block"); </script>
依此弹出的对话框为:
loadjs() exit...
loadjs() finished!
hello world
another block
然后才会显示页面。
script 的defer和async属性
IE自从IE6就之处defer 标签,如:
<script defer type="text/javascript" src="./alert.js" ></script>
对于IE来说,这个标签会让IE并行下载JS文件,并且把其执行hold到了整个DOM装载完毕,多个defer 的<script>在执行时也会按照其出现的顺序来运行。最重要的是<script>被加上refer 后,其不会阻塞后续DOM 的渲染。但是因为refer 只是IE专用,所以一般用的比较少。
我们的HMTL 5也加入了一个异步载入 JavaScript的属性:async 。无论你对它赋什么样的值,只要它出现,它就开始异步加载 JS文件。但是,async的异步加载会有一个比较严重的问题,那就是它忠实的执行“载入后马上执行”这条规则。所以,虽然它并不阻塞页面的渲染,但是你也无法控制他执行的次序和时机(示例)。
支持 async 标签的浏览器如下,Opera还不支持(来自这里),所以这个方法也不是太好。
动态创建DOM的方式
这种方式可能是用的最多的了。
function loadjs(script_filename) { var script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.setAttribute('src', script_filename); script.setAttribute('id', 'coolshell_script_id'); script_id = document.getElementById('coolshell_script_id'); if(script_id){ document.getElementsByTagName('head')[0].removeChild(script_id); } document.getElementsByTagName('head')[0].appendChild(script); } var script = 'http://coolshell.cn/asyncjs/alert.js'; loadjs(script);
这种方式几乎成了标准的异步载入js文件的方式(示例)。这种方式还玩出了 jsonp 的东东。也就是我们可以为script的src 指定某个后台的脚本(比如PHP),而这个PHP返回一个JavaScript函数,其参数是一个json 字符串,返回来调用我们预先定义好的 JavaScript 函数。作者的参考示例:t.js (这个示例是作者之前在微博征集的一个异步ajax调用的小例子)
按需异步载入JS
上面的DOM方式的例子解决了异步载入JavaScript的问题,但是没有解决我们想让他按我指定的时机运行的问题。所以,我们需要把上面的DOM方式绑定到某个事件上就可以了。
比如:
1) 绑在window.load 事件上(示例)
window.load = loadjs("http://coolshell.cn/asyncjs/alert.js")
2) 绑在特定的事件上(示例)
<p style="cursor: pointer" onclick="LoadJS()">Click to load alert.js </p>
比如当我们在点击某个DOM元素时,才载入我们的JS文件。
更多
有的人可能会觉得绑定在某个特定事件上似乎过了一点,而在点击时才载入JS又太慢了。这里抛出一个终极问题:我们想要异步地把JS文件下载到用户本地,但是又不执行,仅当我们想要执行的时候才去执行。
作者提出了一种方式,就像多年之前玩preload图片那样,我们可以动用 object 标签(也可以使用 iframe 标签),于是有了下面的代码(示例):
function cachejs(script_filename){ var cache = document.createElement('object'); cache.data = script_filename; cache.id = "coolshell_script_cache_id"; cache.width = 0; cache.height = 0; document.body.appendChild(cache); }
在Chrome 下按F12(或者Ctrl+Shit+I),切换到 network页,可以看到 alert.js 文件已经下载了但是却没有执行弹出 "hello,world"对话框的操作。然后我们再用之前“绑在特定的事件上”的方式,因为浏览器端有缓存了,不会在从服务器上下载 alert.js 文件了,这样就能保证执行速度了。
我们还可以用Ajax的方式,比如:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'new.js'); xhr.send('');