探析浏览器执行JavaScript脚本加载与代码执行顺序


Posted in Javascript onJanuary 12, 2016

本文主要基于向HTML页面引入JavaScript的几种方式,分析HTML中JavaScript脚本的执行顺序问题

1. 关于JavaScript脚本执行的阻塞性

JavaScript在浏览器中被解析和执行时具有阻塞的特性,也就是说,当JavaScript代码执行时,页面的解析、渲染以及其他资源的下载都要停下来等待脚本执行完毕①。这一点是没有争议的,并且在所有浏览器中的行为都是一致的,原因也不难理解:浏览器需要一个稳定的DOM结构,而JavaScript可能会修改DOM(改变DOM结构或修改某个DOM节点),如果在JavaScript执行的同时还继续进行页面的解析,那么整个解析过程将变得难以控制,解析出错的可能也变得很大。

然而这里还有一个问题需要注意,对于外部脚本,还涉及到一个脚本下载的过程,在早期的浏览器中,JavaScript文件的下载不仅会阻塞页面的解析,甚至还会阻塞页面其他资源的下载(包括其他JavaScript脚本文件、外部CSS文件以及图片等外部资源)。从IE8、firefox3.5、safari4和chrome2开始允许JavaScript并行下载,同时JavaScript文件的下载也不会阻塞其他资源的下载(旧版本中,JavaScript文件的下载也会阻塞其他资源的下载)。

注:不同浏览器对于同一个域名下的最大连接数有不同的限制,HTTP1.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脚本的执行顺序也是从上到下的,即页面上先出现的代码或先被引入的代码总是被先执行,即使是允许并行下载JavaScript文件时也是如此。注意我们这里标红了"正常情况下",原因是什么呢?我们知道,在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秒才会返回,但是在所有浏览器中,弹出的顺序也都是相同的,即:"我是第一个脚本"->"我是内部脚本"->"我是第二个脚本"

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将被修改为:

探析浏览器执行JavaScript脚本加载与代码执行顺序

而代码执行的结果也符合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://3water.com/article/26446.htm

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注入的脚本肯定是按照添加的顺序执行;反之,如果我们采用异步的方案,那么添加的脚本的执行顺序肯定是无法确定的。

Javascript 相关文章推荐
jquery下组织javascript代码(js函数化)
Aug 25 Javascript
javascript 保存文件到本地实现方法
Nov 29 Javascript
关于textarea提交的内容无法换行的解决办法
Apr 09 Javascript
解析javascript 浏览器关闭事件
Jul 08 Javascript
js函数名与form表单元素同名冲突的问题
Mar 07 Javascript
JavaScript及jquey实现多个数组的合并操作
Sep 06 Javascript
原创jQuery弹出层插件分享
Apr 02 Javascript
JS模拟bootstrap下拉菜单效果实例
Jun 17 Javascript
vue中如何实现变量和字符串拼接
Jun 19 Javascript
如何为vue的项目添加单元测试
Dec 19 Javascript
Vue函数式组件-你值得拥有
May 09 Javascript
JavaScript布尔运算符原理使用解析
May 06 Javascript
学习JavaScript设计模式之策略模式
Jan 12 #Javascript
基于jQuery1.9版本如何判断浏览器版本类型
Jan 12 #Javascript
jQuery版本升级踩坑大全
Jan 12 #Javascript
基于jQuery实现点击最后一行实现行自增效果的表格
Jan 12 #Javascript
7个jQuery最佳实践
Jan 12 #Javascript
实例详解jQuery Mockjax 插件模拟 Ajax 请求
Jan 12 #Javascript
JavaScript实现输入框(密码框)出现提示语
Jan 12 #Javascript
You might like
索尼SONY SRF-S83/84电路分析和打磨
2021/03/02 无线电
PHP中使用asort进行中文排序失效的问题处理
2014/08/18 PHP
PHP正则替换函数preg_replace()报错:Notice Use of undefined constant的解决方法分析
2017/02/04 PHP
tp5框架前台无限极导航菜单类实现方法分析
2020/03/29 PHP
jQuery中的.bind()、.live()和.delegate()之间区别分析
2011/06/08 Javascript
js判断60秒以及倒计时示例代码
2014/01/24 Javascript
JQuery与JS里submit()的区别示例介绍
2014/02/17 Javascript
javascript动态设置样式style实例分析
2015/05/13 Javascript
apply和call方法定义及apply和call方法的区别
2015/11/15 Javascript
深入探究AngularJS框架中Scope对象的超级教程
2016/01/04 Javascript
弹出遮罩层后禁止滚动效果【实现代码】
2016/04/29 Javascript
nodejs的压缩文件模块archiver用法示例
2017/01/18 NodeJs
基于JavaScript实现活动倒计时效果
2017/04/20 Javascript
如何使用JS在HTML中自定义字符串格式化
2017/07/20 Javascript
JS实现验证码倒计时的注册页面
2018/01/02 Javascript
vue 指定组件缓存实例详解
2018/04/01 Javascript
服务端预渲染之Nuxt(使用篇)
2019/04/08 Javascript
详解微信小程序调用支付接口支付
2019/04/28 Javascript
公众号SVG动画交互实战代码
2020/05/31 Javascript
多个Vue项目部署到服务器的步骤记录
2020/10/22 Javascript
python安装cx_Oracle模块常见问题与解决方法
2017/02/21 Python
Django中使用Whoosh进行全文检索的方法
2019/03/31 Python
python实现人机猜拳小游戏
2020/02/03 Python
Python 生成VOC格式的标签实例
2020/03/10 Python
Python3-异步进程回调函数(callback())介绍
2020/05/02 Python
Django ModelForm组件原理及用法详解
2020/10/12 Python
利用python+ffmpeg合并B站视频及格式转换的实例代码
2020/11/24 Python
CSS3中各种颜色属性的使用教程
2016/05/17 HTML / CSS
吃空饷专项治理工作实施方案
2014/03/04 职场文书
2015年商场工作总结
2015/04/27 职场文书
简爱电影观后感
2015/06/10 职场文书
golang 定时任务方面time.Sleep和time.Tick的优劣对比分析
2021/05/05 Golang
python munch库的使用解析
2021/05/25 Python
GitHub上77.9K的Axios项目有哪些值得借鉴的地方详析
2021/06/15 Javascript
浅谈Python魔法方法
2021/06/28 Java/Android
macos系统如何实现微信双开? mac登录两个微信以上微信的技巧
2022/07/23 数码科技