探析浏览器执行JavaScript脚本加载与代码执行顺序
本文深入分析HTML页面中JavaScript脚本的加载与执行机制。重点探讨脚本执行的阻塞特性、并行下载限制,以及正常引入、document.write注入、动态脚本和Ajax注入四种方式下的具体执行顺序差异,特别是不同浏览器在混合使用内部与外部脚本时的行为区别。
JavaScript脚本加载与执行顺序概述
文章探讨HTML页面引入JavaScript的多种途径,解析其中脚本的执行时序。
脚本执行的阻塞性与并行下载
1. 探讨JavaScript脚本执行的阻塞行为
浏览器解析执行JavaScript时具备阻塞特性,即脚本运行期间,页面解析、渲染及资源下载均需暂停,直至脚本执行结束①。此行为在所有浏览器中一致且无争议,原因在于浏览器需维持稳定的DOM结构,而JavaScript可能修改DOM,若解析与脚本执行并发,将导致解析失控且易出错。
需注意的是,外部脚本涉及下载过程。早期浏览器中,JavaScript文件下载不仅阻塞页面解析,还阻塞其他资源(如其他JS、CSS及图片)的下载。自IE8、Firefox 3.5、Safari 4及Chrome 2起,支持JavaScript并行下载,且不再阻塞其他资源下载(旧版本中JS下载会阻塞其他资源)。
注:各浏览器对同一域名的最大连接数限制不同,HTTP 1.1规范要求不超过2个,但多数浏览器实际支持超过2个,IE6/7为2个,IE8提升至6个,Firefox和Chrome亦为6个,该设置可修改,详情见:http://www.stevesouders.com/blog/2008/03/20/roundup-on-parallel-connections/
脚本执行顺序的影响因素
2. 探讨脚本的执行顺序
浏览器按从上至下顺序解析页面,故正常情况下JavaScript脚本亦按此顺序执行,即先出现或先引入的代码先执行,即便允许并行下载JS文件亦然。此处强调“正常情况下”,原因在于HTML引入JavaScript的方式多样,概括如下(不考虑requirejs或seajs等模块加载器):
(1)常规引入:通过页面中的<script>标签引入脚本代码或外部脚本。
(2)利用document.write方法向页面写入<script>标签或代码。
(3)利用动态脚本技术,即通过DOM接口创建<script>元素,设置其src属性,再将其加入DOM。
(4)通过Ajax获取脚本内容,随后创建<script>元素,设置其text属性,再将元素加入DOM。
(5)直接将JavaScript代码置于元素的事件处理程序中或作为URL主体,示例如下:
<!--直接写在元素的事件处理程序中-->
<input type="button" value="点击测试一下" onclick="alert('点击了按钮')"/>
<!--作为URL的主体-->
<a href="javascript:alert('dd')">JS脚本作为URL的主体</a>
第5种情况对讨论脚本执行顺序无影响,故仅讨论前四种情况:
正常引入脚本的执行顺序
2.1 常规引入脚本时
常规引入脚本时,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注入脚本的执行顺序
2.2 通过document.write向页面写入脚本时
在文档流未关闭时,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引入的代码执行顺序必在之前标签代码之后。
动态脚本技术的执行特性
2.3 通过动态脚本技术添加代码时
通过动态脚本技术添加代码主要目的在于创建无阻塞脚本,因通过动态脚本技术添加的代码不会立即执行,可通过下述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()方法实现,详情见:https://www.jb51.net/article/26446.htm
Ajax注入脚本的执行顺序
2.4 通过Ajax注入脚本
通过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]);
}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注入的脚本必按添加顺序执行;反之,若采用异步方案,则添加脚本的执行顺序必无法确定。
JS监听滚动事件:回到顶部按钮的显示与隐藏逻辑
2026-11-05 16:19:37
JavaScript怎么读写LocalStorage与SessionStorage
2026-11-05 15:40:11
anime.js 与 GSAP 对比:轻量级项目该怎么选择动画库
2026-11-05 11:24:55
flv.js 与 hls.js 在直播场景下的选型对比
2026-11-05 08:18:52
JavaScript能做什么?前端开发中的作用与应用场景解析
2026-11-04 23:17:54
