本文主要基于向HTML页面引入JavaScript的几种方式,分析HTML中JavaScript脚本的执行顺序问题javascript
JavaScript在浏览器中被解析和执行时具备阻塞的特性,也就是说,当JavaScript代码执行时,页面的解析、渲染以及其余资源的下载都要停下来等待脚本执行完毕①。这一点是没有争议的,而且在全部浏览器中的行为都是一致的,缘由也不难理解:浏览器须要一个稳定的DOM结构,而JavaScript可能会修改DOM(改变DOM结构或修改某个DOM节点),若是在JavaScript执行的同时还继续进行页面的解析,那么整个解析过程将变得难以控制,解析出错的可能也变得很大。php
然而这里还有一个问题须要注意,对于外部脚本,还涉及到一个脚本下载的过程,在早期的浏览器中,JavaScript文件的下载不只会阻塞页面的解析,甚至还会阻塞页面其余资源的下载(包括其余JavaScript脚本文件、外部CSS文件以及图片等外部资源)。从IE八、firefox3.五、safari4和chrome2开始容许JavaScript并行下载,同时JavaScript文件的下载也不会阻塞其余资源的下载(旧版本中,JavaScript文件的下载也会阻塞其余资源的下载)。html
注:不一样浏览器对于同一个域名下的最大链接数有不一样的限制,HTTP1.1协议规范中的要求是不能高于2个,可是大多数浏览器目前实际提供的最大链接数都多于2个,IE6/7都是2个,IE8提高到了6个,firefox和chrome也是6个,固然这个设置也是能够修改的,详细内容能够参考:http://www.stevesouders.com/blog/2008/03/20/roundup-on-parallel-connections/java
浏览器是按照从上到下的顺序解析页面,所以正常状况下,JavaScript脚本的执行顺序也是从上到下的,即页面上先出现的代码或先被引入的代码老是被先执行,即便是容许并行下载JavaScript文件时也是如此。注意咱们这里标红了"正常状况下",缘由是什么呢?咱们知道,在HTML中加入JavaScript代码有多种方式,归纳以下(不考虑requirejs或seajs等模块加载器):chrome
(1)正常引入:即在页面中经过<script>标签引入脚本代码或者引入外部脚本浏览器
(2)经过document.write方法向页面写入<script>标签或代码app
(3)经过动态脚本技术,即利用DOM接口建立<script>元素,并设置元素的src,而后再将元素添加进DOM中。异步
(4)经过Ajax获取脚本内容,而后再建立<script>元素,并设置元素的text,再将元素添加进DOM中。async
(5)直接把JavaScript代码写在元素的事件处理程序中或直接做为URL的主体,示例以下:函数
<!--直接写在元素的事件处理程序中--> <input type="button" value="点击测试一下" onclick="alert('点击了按钮')"/> <!--做为URL的主体--> <a href="javascript:alert('dd')">JS脚本做为URL的主体</a>
第5种状况对于咱们讨论的脚本执行顺序没有什么影响,所以咱们这里只讨论前四种状况:
正常引入脚本时,JavaScript代码会按照从上到下的顺序执行,无论脚本是否是并行下载,执行时仍是按照引入的顺序从上到下执行的,咱们如下面的DEMO为例:
首先,经过PHP写了一个脚本,这个脚本接收两个参数,文件URL和延迟时间,脚本会在传入的延迟时间以后,将文件内容发送给浏览器,脚本以下:
<?php $url = $_GET['url']; $delay = $_GET['delay']; if(isset($delay)){ sleep($delay); } echo file_get_contents($url); ?>
另外咱们还定义了两个JavaScript文件,分别为1.js和2.js,在这个例子中,两者的代码分别以下:
1.js
alert("我是第一个脚本");
2.js
alert("我是第二个脚本");
而后,咱们在HTML中引入脚本代码:
<script src='/delayfile.php?url=http://localhost/js/load/1.js&delay=3' type='text/javascript'></script> <script type="text/javascript"> alert("我是内部脚本"); </script> <script src='/delayfile.php?url=http://localhost/js/load/2.js&delay=1' type='text/javascript'></script>
虽然第一个脚本延迟了3秒才会返回,可是在全部浏览器中,弹出的顺序也都是相同的,即:"我是第一个脚本"->"我是内部脚本"->"我是第二个脚本"
document.write在文档流没有关闭的状况下,会将内容写入脚本所在位置结束以后紧邻的位置,浏览器执行完当前短的代码,会接着解析document.write所写入的内容。
注:document.write写入内容的位置还存在一个问题,加入在<head>内部的脚本中写入了<head>标签内部不该该出现的内容,好比<div>等内容标签等,则这段内容的起始位置将是<body>标签的起始位置。
经过document.write写入脚本时存在一些问题,须要分类进行说明:
[1]同一个<script>标签中经过document.write只写入外部脚本:
在这种状况下,外部脚本的执行顺序老是低于引入脚本的标签内的代码,而且按照引入的顺序来执行,咱们修改HTML中的代码:
<script src='/delayfile.php?url=http://localhost/js/load/1.js&delay=2' type='text/javascript'></script> <script type="text/javascript"> document.write('<script type="text/javascript" src="/delayfile.php?url=http://localhost/js/load/2.js"><\/script>'); document.write('<script type="text/javascript" src="/delayfile.php?url=http://localhost/js/load/1.js"><\/script>'); alert("我是内部脚本"); </script>
这段代码执行完毕以后,DOM将被修改成:
而代码执行的结果也符合DOM中脚本的顺序:"我是第一个脚本"->"我是内部脚本"->"我是第二个脚本"->"我是第一个脚本"
[2]同一个<script>标签中经过document.write只写入内部脚本:
在这种状况下,经过documen.write写入的内部脚本,执行顺序的优先级与写入脚本标签内的代码相同,而且按照写入的前后顺序执行:
咱们再修改HTML代码以下:
<script src='/delayfile.php?url=http://localhost/js/load/1.js' type='text/javascript'></script> <script type="text/javascript"> document.write('<script type="text/javascript">alert("我是docment.write写入的内部脚本")<\/script>'); alert("我是内部脚本"); document.write('<script type="text/javascript">alert("我是docment.write写入的内部脚本2222")<\/script>'); document.write('<script type="text/javascript">alert("我是docment.write写入的内部脚本3333")<\/script>'); </script>
在这种状况下,document.write写入的脚本被认为与写入位置处的代码优先级相同,所以在全部浏览器中,弹出框的顺序均为:"我是第一个脚本"->"我是document.write写入的内部脚本"->"我是内部脚本"->"我是document.write写入的内部脚本2222"->"我是document.write写入的内部脚本3333"
[3]同一个<script>标签中经过document.write同时写入内部脚本和外部脚本时:
在这种状况下,不一样的浏览器中存在一些区别:
在IE9及如下的浏览器中:只要是经过document.write写入的内部脚本,其优先级老是高于document.write写入的外部脚本,而且优先级与写入标签内的代码相同。而经过经过document.write写入的外部脚本,则老是在写入标签的代码执行完毕后,再按照写入的顺序执行;
而在其中浏览器中, 出如今第一个document.write写入的外部脚本以前的内部脚本,执行顺序的优先级与写入标签内的脚本优先级相同,而以后写入的脚本代码,不论是内部脚本仍是外部脚本,老是要等到写入标签内的脚本执行完毕后,再按照写入的顺序执行。
咱们修改如下HTML中的代码:
<script src='/delayfile.php?url=http://localhost/js/load/1.js' type='text/javascript'></script> <script type="text/javascript"> document.write('<script type="text/javascript">alert("我是docment.write写入的内部脚本")<\/script>'); alert("我是内部脚本"); document.write('<script type="text/javascript" src="/delayfile.php?url=http://localhost/js/load/1.js"><\/script>'); document.write('<script type="text/javascript">alert("我是docment.write写入的内部脚本2222")<\/script>'); document.write('<script type="text/javascript" src="/delayfile.php?url=http://localhost/js/load/1.js"><\/script>'); document.write('<script type="text/javascript">alert("我是docment.write写入的内部脚本3333")<\/script>'); alert("我是内部脚本2222"); </script>
在IE9及如下的浏览器中,上面代码执行后弹出的内容为:"我是第一个脚本"->"我是document.write写入的内部脚本"->"我是内部脚本"->"我是document.write写入的内部脚本2222"->"我是document.write写入的内部脚本3333"->"我是内部脚本2222"->"我是第一个脚本"->"我是第一个脚本"
其余浏览器中,代码执行后弹出的内容为:"我是第一个脚本"->"我是document.write写入的内部脚本"->"我是内部脚本"->"我是内部脚本2222"->"我是第一个脚本"->"我是document.write写入的内部脚本2222"->"我是第一个脚本"->"我是document.write写入的内部脚本3333"
若是但愿IE及如下的浏览器与其余浏览器保持一致的行为,那么可选的作法就是把引入内部脚本的代码拿出来,单独放在后面一个新的<script>标签内便可,由于后面<script>标签中经过document.write所引入的代码执行顺序确定是在以前的标签中的代码的后面的。
经过动态脚本技术添加代码的主要目的在于建立无阻塞脚本,由于经过动态脚本技术添加的代码不会马上执行,咱们能够经过下面的load函数为页面添加动态脚本:
function loadScript(url,callback){ var script = document.createElement("script"); script.type = "text/javascript"; //绑定加载完毕的事件 if(script.readyState){ script.onreadystatechange = function(){ if(script.readyState === "loaded" || script.readyState === "complete"){ callback&&callback(); } } }else{ script.onload = function(){ callback&&callback(); } } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
可是经过动态脚本技术添加的外部JavaScript脚本不保证按照添加的顺序执行,这一点能够经过回调或者使用jQuery的html()方法,详细可参考:http://www.cnblogs.com/sanshi/archive/2011/02/28/1967367.html
经过Ajax注入脚本一样也是添加无阻塞脚本的技术之一,咱们首先须要建立一个XMLHttpRequest对象,而且实现get方法,而后经过get方法取得脚本内容并注入到文档中。
代码示例:
咱们能够用以下代码封装XMLHttpRequest对象,并封装其get方法:
var xhr = (function(){ function createXhr(){ var xhr ; if(window.XMLHttpRequest){ xhr = new XMLHttpRequest(); }else if(window.ActiveXObject){ var xhrVersions = ['MSXML2.XMLHttp','MSXML2.XMLHttp.3.0','MSXML2.XMLHttp.6.0'], i, len; for(i = 0, len = xhrVersions.length; i < len ; i++){ try{ xhr = new ActiveXObject(xhrVersions[i]); if(xhr){ break; } }catch(e){ } } }else{ throw new Error("没法建立xhr对象"); } return xhr; } function get(url,async,callback){ var xhr = createXhr(); xhr.onreadystatechange = function(){ if(xhr.readyState == 4){ if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){ callback&&callback(xhr.responseText); }else{ alert("请求失败,错误码为" + xhr.status); } } } xhr.open("get",url,async); xhr.send(null); } return { get:get } }())
而后基于xhr对象,再建立loadXhrScript函数:
function loadXhrScript(url,async, callback){ if(async == undefined){ async = true; } xhr.get(url,async,function(text){ var script = document.createElement("script"); script.type = "text/javascript"; script.text = text; document.body.appendChild(script); }); }
咱们上面的get方法添加了一个参数,便是否异步,那么若是咱们采用同步方法,经过Ajax注入的脚本确定是按照添加的顺序执行;反之,若是咱们采用异步的方案,那么添加的脚本的执行顺序确定是没法肯定的。