Javascript 装载和执行
一两个月前在淘宝内网里看到一个优化Javascript代码的竞赛,发现有不少的人对Javascript的执行和装载的基础并不懂,所以,从那天起我就想写一篇文章,但一直耽搁了。自上篇《浏览器渲染原理简介》,正好也可以承前启后。
首先,我想说一下Javascript的装载和执行。通常来说,浏览器对于Javascript的运行有两大特性:1)载入后马上执行,2)执行时会阻塞页面后续的内容(包括页面的渲染、其它资源的下载)。于是,如果有多个js文件被引入,那么对于浏览器来说,这些js文件被被串行地载入,并依次执行。
因为javascript可能会来操作HTML文档的DOM树,所以,浏览器一般都不会像并行下载css文件并行下载js文件,因为这是js文件的特殊性造成的。所以,如果你的javascript想操作后面的DOM元素,基本上来说,浏览器都会报错说对象找不到。因为Javascript执行时,后面的HTML被阻塞住了,DOM树时还没有后面的DOM结点。所以程序也就报错了。
目录
传统的方式
所以,当你写在代码中写下如下的代码:
<script type="text/javascript" src="https://sou-ip.com/asyncjs/alert.js"></script>
基本上来说,head里的 <script>标签会阻塞后续资源的载入以及整个页面的生成。我专门做了一个示例你可以看看:示例一。 注意:我的alert.js中只有一句话:alert(“hello world”) ,这更容易让你看到javascript是怎么阻塞后面的东西的。
所以,你知道为什么有很多网站把javascript放在网页的最后面了,要么就是动用了window.onload或是docmuemt ready之类的事件。
另外,因为绝大多数的Javascript代码并不需要等页面,所以,我们异步载入的功能。那么我们怎么异步载入呢?
document.write方式
于是,你可能以为document.write()这种方式能够解决不阻塞的方式。你当然会觉得,document.write了的<script>标签后就可以执行后面的东西去了,这没错。对于在同一个script标签里的Javascript的代码来说,是这样的,但是对于整个页面来说,这个还是会阻塞。 下面是一段测试代码:
<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 = 'https://sou-ip.com/asyncjs/alert.js'; loadjs(script); alert("loadjs() finished!"); </script> <script type="text/javascript" language="javascript"> alert("another block"); </script>
你觉得alert的顺序是什么?你可以在不同的浏览器里试一试。这里的想关的测试页面:示例二。
script的defer和async属性
IE自从IE6就支持defer标签,如:
<script defer type="text/javascript" src="./alert.js" > </script>
对于IE来说,这个标签会让IE并行下载js文件,并且把其执行hold到了整个DOM装载完毕(DOMContentLoaded),多个defer的<script>在执行时也会按照其出现的顺序来运行。最重要的是<script>被加上defer后,其不会阻塞后续DOM的的渲染。但是因为这个defer只是IE专用,所以一般用得比较少。
而我们标准的的HTML5也加入了一个异步载入javascript的属性:async,无论你对它赋什么样的值,只要它出现,它就开始异步加载js文件。但是, async的异步加载会有一个比较严重的问题,那就是它忠实地践行着“载入后马上执行”这条军规,所以,虽然它并不阻塞页面的渲染,但是你也无法控制他执行的次序和时机。你可以看看这个示例去感受一下。
支持 async标签的浏览器是:Firefox3.6+,Chrome 8.0+,Safari 5.0+,IE 10+,Opera还不支持(来自这里)所以这个方法也不是太好。因为并不是所有的浏览器你都能行。
动态创建DOM方式
这种方式可能是用得最多的了。
function loadjs(script_filename) { var script = document.createElement('script'); script.setAttribute('type', 'text/javascript'); script.setAttribute('src', script_filename); script.setAttribute('id', 'sou-ip_script_id'); script_id = document.getElementById('sou-ip_script_id'); if(script_id){ document.getElementsByTagName('head')[0].removeChild(script_id); } document.getElementsByTagName('head')[0].appendChild(script); } var script = 'https://sou-ip.com/asyncjs/alert.js'; loadjs(script);
这个方式几乎成了标准的异步载入js文件的方式,这个方式的演示请参看:示例三。这方式还被玩出了JSONP的东东,也就是我可以为script的src指定某个后台的脚本(如PHP),而这个PHP返回一个javascript函数,其参数是一个json的字符串,返回来调用我们的预先定义好的javascript的函数。你可以看一下这个示例:t.js (这个示例是我之前在微博征集的一个异步ajax调用的小例子)
按需异步载入js
上面那个DOM方式的例子解决了异步载入Javascript的问题,但是没有解决我们想让他按我们指定的时机运行的问题。所以,我们只需要把上面那个DOM方式绑到某个事件上来就可以了。
比如:
绑在window.load事件上——示例四
你一定要比较一下示例四和示例三在执行上有什么不同,我在这两个示例中都专门用了个代码高亮的javascript,看看那个代码高亮的的脚本的执行和我的alert.js的执行的情况,你就知道不同了)
window.load = loadjs("https://sou-ip.com/asyncjs/alert.js")
绑在特定的事件上——示例五
<p style="cursor: pointer" onclick="LoadJS()">Click to load alert.js </p>
这个示例很简单了。当你点击某个DOM元素,才会真正载入我们的alert.js。
更多
但是,绑定在某个特定事件上这个事似乎又过了一点,因为只有在点击的时候才会去真正的下载js,这又会太慢了了。好了,到这里,要抛出我们的终极问题——我们想要异步地把js文件下载到用户的本地,但是不执行,仅当在我们想要执行的时候去执行。
要是我们有下面这样的方式就好了:
var script = document.createElement("script"); script.noexecute = true; script.src = "alert.js"; document.body.appendChild(script); //后面我们可以这么干 script.execute();
可惜的是,这只是一个美丽的梦想,今天我们的Javascript还比较原始,这个“JS梦”还没有实现呢。
所以,我们的程序员只能使用hack的方式来搞。
有的程序员使用了非标准的script的type来cache javascript。如:
<script type=cache/script src="./alert.js"></script>
因为”cache/script”,这个东西根本就不能被浏览器解析,所以浏览器也就不能把alert.js当javascript去执行,但是他又要去下载js文件,所以就可以搞定了。可惜的是,webkit严格符从了HTML的标准——对于这种不认识的东西,直接删除,什么也不干。于是,我们的梦又破了。
所以,我们需要再hack一下,就像N多年前玩preload图片那样,我们可以动用object标签(也可以动用iframe标签),于是我们有下面这样的代码:
function cachejs(script_filename){ var cache = document.createElement('object'); cache.data = script_filename; cache.id = "sou-ip_script_cache_id"; cache.width = 0; cache.height = 0; document.body.appendChild(cache); }
然后,我们在的最后调用一下这个函数。请参看一下相关的示例:示例六
在Chrome下按 Ctrl+Shit+I,切换到network页,你就可以看到下载了alert.js但是没有执行,然后我们再用示例五的方式,因为浏览器端有缓存了,不会再从服务器上下载alert.js了。所以,就能保证执行速度了。
关于这种preload这种东西你应该不会陌生了。你还可以使用Ajax的方式,如:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'new.js'); xhr.send('');
到这里我就不再多说了,也不给示例了,大家可以自己试试去。
最后再提两个js,一个是ControlJS,一个叫HeadJS,专门用来做异步load javascript文件的。
好了,这是所有的内容了,希望大家看过后能对Javascript的载入和执行,以及相关的技术有个了解。同时,也希望各前端高手不吝赐教!
(全文完)
(转载本站文章请注明作者和出处 宝酷 – sou-ip ,请勿用于任何商业用途)
《Javascript 装载和执行》的相关评论
老大是全才,啥都懂啊。沙发得坐啊。
再来坐个地板!
谢谢分享,文章写得细致深入,很受教!
想问一下,实例6里面,为什么要用object元素而不是用其他的div,span之类的元素呢?
谢谢!
示例六的html页面是不是有点问题,出现了两次cachejs调用;
loadjs没有重新请求js文件是不是浏览器从缓存中获取的js内容?如果加载的js的url是类似aa.js?v=20120202是不是还会重新请求还是走缓存?上班了,等有时间我测试一下
在测试第二个例子的时候,chrome与firefox中,行为不一样.
在chrome(27.0.1453.94 m)中:
1.loadjs() exit …
2.loadjs() finished!
3. hello world
4. another block
在firefox(v21.0)中
1.hello world
2. loadjs() exit…
3. loadjs() finished!
4. another block
这篇内容简单易懂,我这个前端盲都获益不少。不过这次你文章中错别字太多了,休息不够是吗?
有一种常见的需求是,js并发下载,但是顺序执行:http://www.raychase.net/123
其实预加载最后一种ajax最顺畅,前面hack方式都太绕了。反正代码来实现动态加载,不用纠结一定要用htmlTag,所谓的script.execute,一直有,ie下可以execScript方法,也支持通用方法eval。一定要用script element ,就把ajax的responseText设置到script.text再append也可。
确实是这个样子,测试了一下
iframe标签也可以做到异步加载js文件
关注一段时间了,做嵌入式的。发现LZ功底涉及面很广,像我们做silicon级的完全不懂上层,OS底层涉及较多,相对server简单吧,也从LZ的blog学到很多。想问与本文无关两个问题:(1)职业IT技术就这样一直忙下去吗?没时间陪家人吗?我是想两边都兼顾的。(2)有什么小技巧能share给我们这些后生的 少走走弯路 就像你一样,成为内外兼修之才。
小白,提个 t.js 的问题,事先知道总数,为什么不先初始好30个div,返回结果直接填进去不就保持顺序了?
又一次看到因为js语法、实现的限制,开发者不得不做的反复折腾。。。。。
这么多互联网巨头、浏览器厂商,就不能把js改进得自然方便一点吗?
@haitao
html5的标准讨论了那么久都还没有完全确立,要改进js恐怕遥遥无期……
把JS写在的结尾怎么样?
把JS写在body的结尾怎么样?
耗哥懂得不是一般的多啊
继续顶
我们项目一直使用LABjs, 这是一个特别成熟的小项目,特别适合有复杂依赖的js文件异步导入的项目
这种基础的东西,很少开发者会去深入研究,耗哥对技术的态度实在值得大家学习!
之前我也总结的javascript引入页面的方式: http://steeeeps.net/2012/12/15/javascript-load/ ,呵呵,
@由 LABjs是个不错的库,我自己在项目里也用过,不过这个项目主页上说不会再有进一步的开发了。
现在的js开发框架(如:jQuery),对程序加载顺序写有改进啊;耗哥
异步是解决在不阻塞当前”主要”处理任务的情况下,执行其他处理任务的需求的方法.
@darklonely
本文中说到的消除阻塞,个人认为是:用来提高用户使用体验,尽快完成页面生成任务,尽早地给用户一种”可以开始进行交互”的感觉的需求.而完成页面生成任务中关键的一部是,尽快的分析处理完html文档.
之所以说它是”需求”,而不是”任务”,是因为在XML中所有标签应该是平等的,不同标签代表的意义,所需要的处理操作可能不同.但是归于一点,它们都是”标签”.流文档的特性是,一个标签分析处理完成后,再处理另一个标签,直至所有标签处理完毕.
浏览器是根据”HTML DOM”这种数据结构来进行页面的生成操作.不同于其他标签,标签因为它的”底层性”,提供了直接与该数据结构交互的能力.由于这种其他标签所不具备的操作上的”灵活性”与功能上的”强大性”,人们往往”过度”的使用标签:用它进行大量的操作,并且因为”载入后马上执行”这条军规,”很容易不自觉地”造成阻塞整个页面生成流程的后果,最终影响用户体验.
所以单纯的获取数据这个角度上考量,去除数据大小,网络状况等因素后,单个//的性能应该是一样的.的异步需求,主要是由于它的”执行”特性,影响生成页面速度造成的.
不知这么理解是否正确.望指正.
@darklonely
晕,把script,object,img等标签给吃了….不带尖括号就对了.
require.js 怎么样
我觉得现有的工具已经很成熟, 比如说就我来说执行时机有jquery的ready控制就挺够用了. 有了妥善的方案之后再去研究其它各种方案的具体细节, 不是说没有价值, 但这恐怕对于一般js使用者来说使用价值是完全没有了. 再比如文中纠结很多的那个被加载者的执行时机问题, 就不能从被调用者身上着手么, 里面只写函数定义不写裸的语句, 调用者不就能完全控制执行时机了?
现在不是都用CMD么,比如dojo就支持的很好啊
@Mr.Wiredan 刚刚去查了一下,没有看见说不更新了啊!labjs还是特别稳定了,尤其是在国内各种低版本浏览器盛行的情况下。
皓哥的文章写很好,但我有几点建议,第一,示例代码中script的language属性是没有必要的可以去掉。
第二,好像不仅仅只有IE才支持defer属性,我们可以通过一下网站测试我们的浏览器是否支持defer属性,
第三,随便一下RequestJS也是支持异步加载,谢谢。
http://browserspy.dk/javascript.php
写这么多麻烦的干嘛,直接把script标签写到页面的最后不就都解决了
alipay的玉伯的团队不是写了seajs吗?
测试了一和#5和#9结果相同,另IE和Chrome结果相同,独独Firefox让人吃惊。
+1
其实labjs的文件加载的几种实现就是博主说的这些,如果只是管理js文件加载的问题建议采用labjs,但是需要模块管理的话建议requirejs或淘宝的seajs
示例二 loadjs(script); 的时候为什么dom里write()生成的js代码是在第一段script结束后的?按照js立即执行的方法不应该在执行alert(“loadjs() finished!”);之前write(),最后成为一个这样的怪东西吗
这样的
<script type=”text/javascript” language=”javascript”>
function loadjs(script_filename) {
document.write(”);
document.write(”);
alert(“loadjs() exit…”);
}
var script = ‘http://sou-ip.com/asyncjs/alert.js’;
loadjs(script);
<script type=”text/javascript” language=”javascript” src=”http://sou-ip.com/asyncjs/alert.js”></script>
alert(“loadjs() finished!”);
</script>
学习了
学习,谢谢如此全面的总结与分享
简单写了并下下载的脚本,我觉得还是蛮方便的。。。
当所有的脚本并下下载完毕后,执行一个回调
(function( $, win, undefined ){
$.loadScripts = function( urlArrays, callback ){
urlArrays = $.isArray( urlArrays ) ? urlArrays : [ urlArrays ];
callback = $.isFunction( callback ) ? callback : $.noop
var loadScript = function( url ) {
var deferred = $.Deferred(),
script = document.createElement(“script”);
script.type = “text/javascript”;
script.src = url;
script.onload = script.onreadystatechange = function() {
if( !this.readyState || this.readyState === “loaded” || this.readyState === “completed”) {
deferred.resolve();
}
}
document.getElementsByTagName(“head”)[0].appendChild( script );
return deferred.promise();
};
var deferredObject = $.map( urlArrays, function( url ){
return loadScript( url );
});
$.when.apply( $, deferredObject).then( callback );
}
})( jQuery, window );
为什么写什么之前都要加上很多人都不懂…
也应该是很多人不懂吧。
要是老大录些普及的视频就好了。现在网上好多高手都在录制视频教程,老大要是能将C++和LINUX编程录制下就好了。
http://sou-ip.com/asyncjs/async_test01.async.html
goodby world