JavaScript异步加载浅析


Posted in Javascript onDecember 28, 2014

前言

关于JavaScript脚本加载的问题,相信大家碰到很多。主要在几个点——

1> 同步脚本和异步脚本带来的文件加载、文件依赖及执行顺序问题
2> 同步脚本和异步脚本带来的性能优化问题

深入理解脚本加载相关的方方面面问题,不仅利于解决实际问题,更加利于对性能优化的把握并执行。
 
先看随便一个script标签代码——

<script src="js/myApp.js"></script>

如果放在<head>上面,会阻塞所有页面渲染工作,使得用户在脚本加载完毕并执行完毕之前一直处于“白屏死机”状态。而<body>末尾的打脚本只会让用户看到毫无生命力的静态页面,原本应该进行客户端渲染的地方却散布着不起作用的控件和空空如也的方框。拿一个测试用例——

<!DOCTYPE html>

<html>

<head lang="en">

    <meta charset="UTF-8">

    <title>异步加载script</title>

    <script src="js/test.js"></script>

</head>

<body>

    <div>我是内容</div>

    <img src="img/test.jpg">

</body>

</html>

其中,test.js中的内容——

alert('我是head里面的脚本代码,执行这里的js之后,才开始进行body的内容渲染!');

我们会看到,alert是一个暂停点,此时,页面是空白的。但是要注意,此时整个页面已经加载完毕,如果body中包含某些src属性的标签(如上面的img标签),此时浏览器已经开始加载相关内容了。总之要注意——js引擎和渲染引擎的工作时机是互斥的(一些书上叫它为UI线程)。

因此,我们需要——那些负责让页面更好看、更好用的脚本应该立即加载,而那些可以待会儿再加载的脚本稍后再加载。

一、脚本延迟执行

现在越来越流行把脚本放在页面<body>标签的尾部。这样,一方面用户可以更快地看到页面,另一方面脚本可以直接操作已经加载完成的dom元素。对于大多数脚本而言,这次“搬家”是个巨大的进步。该页面模型如下——

<!DOCTYPE html>

<html>

<head lang="en">

    <!--metadata and scriptsheets go here-->

    <script src="headScript.js"></script>

</head>

<body>

    <!--content goes here-->

    <script src="bodyScript.js"></script>

</body>

</html>

这确实大大加快了页面的渲染时间,但是注意一点,这可能让用户有机会在加载bodyScript之前与页面交互。源于浏览器在加载完整个文档之前无法加载这些脚本,这对那些通过慢速连接传送的大型文档来说会是一大瓶颈。

理想情况下,脚本的加载应该与文档的加载同时进行,并且不影响DOM的渲染。这样,一旦文档就绪就可以运行脚本,因为已经按照<script>标签的次序加载了相应脚本。

我们使用defer便能够完成这样的需求,即——

<script src="deferredScript.js"></script>

添加defer属性相当于告诉浏览器:请马上开始加载这个脚本吧,但是,请等到文档就绪且此前所有具有defer属性的脚本都结束运行之后再运行它。

这样,在head标签里放入延迟脚本,技能带来脚本置于body标签时的所有好处,又能让大文档的加载速度大幅提升。此时的页面模式便是——

<!DOCTYPE html>

<html>

<head lang="en">

    <!--metadata and scriptsheets go here-->

    <script src="headScript.js"></script>

    <script src="deferredScript.js" defer></script>

</head>

<body>

    <!--content goes here-->

</body>

</html>

但是并非所有的浏览器都支持defer(对于一些modern浏览器,如果声明defer,其内部脚本将不会执行document.write及DOM渲染操作。IE4+均支持defer属性)。这意味着,如果想确保自己的延迟脚本能在文档加载后运行,就必须将所有延迟脚本的代码都封装在诸如jQuery之$(document).ready之类的结构中。这是值得的,因为差不多97%的访客都能享受到并行加载的好处,同时另外3%的访客仍然能使用功能完整的JavaScript。

二、脚本的完全并行化

让脚本的加载及执行再快一步,我不想等到defer脚本一个接着一个运行(defer让我们想到一种静静等待文档加载的有序排队场景),更不想等到文档就绪之后才运行这些脚本,我想要尽快加载并且尽快运行这些脚本。这里也就想到了HTML5的async属性,但是要注意,它是一种混乱的无政府状态。

例如,我们加载两个完全不相干的第三方脚本,页面没有它们也运行得很好,而且也不在乎它们谁先运行谁后运行。因此,对这些第三方脚本使用async属性,相当于一分钱没花就提升了它们的运行速度。

async属性是HTML5新增的。作用和defer类似,即允许在下载脚本的同时进行DOM的渲染。但是它将在下载后尽快执行(即JS引擎空闲了立马执行),不能保证脚本会按顺序执行。它们将在onload 事件之前完成。

Firefox 3.6、Opera 10.5、IE 9 和 最新的Chrome 和 Safari 都支持 async 属性。可以同时使用 async 和 defer,这样IE 4之后的所有 IE 都支持异步加载,但是要注意,async会覆盖掉defer。

那么此时的页面模型如下——

<!DOCTYPE html>

<html>

<head lang="en">

    <!--metadata and scriptsheets go here-->

    <script src="headScript.js"></script>

    <script src="deferredScript.js" defer></script>

</head>

<body>

    <!--content goes here-->

    <script src="asyncScript1.js" async defer></script>

    <script src="asyncScript2.js" async defer></script>

</body>

</html>

要注意这里的执行顺序——各个脚本文件加载,接着执行headScript.js,紧接着在DOM渲染的同时会在后台加载defferedScript.js。接着在DOM渲染结束时将运行defferedScript.js和那两个异步脚本,要注意对于支持async属性的浏览器而言,这两个脚本将做无序运行。

三、可编程的脚本加载

尽管上面两个脚本属性的功能非常吸引人,但是由于兼容性的问题,应用并不是很广泛。故此,我们更多使用脚本加载其他脚本。例如,我们只想给那些满足一定条件的用户加载某个脚本,也就是经常提到的“懒加载”。

在浏览器API层面,有两种合理的方法来抓取并运行服务器脚本——

1> 生成ajax请求并用eval函数处理响应

2> 向DOM插入<script>标签

后一种方式更好,因为浏览器会替我们操心生成HTTP请求这样的事。再者,eval也有一些实际问题:泄露作用域,调试搞得一团糟,而且还可能降低性能。因此,想要加载名为feture.js的脚本,我们应该使用类似下面的代码:

var head = document.getElementsByTagName('head')[0];

var script = document.createElement('script');

script.src = 'feature.js';

head.appendChild(script);

当然,我们要处理回调监听,HTML5规范定义了一个可以绑定回调的onload属性。

script.onload = function() {

    console.log('script loaded ...');

}

不过,IE8及更老的版本并不支持onload,它们支持的是onreadystatechange。而且,对于错误处理仍然千奇百怪。在这里,可以多参考一些流行的校本加载库,如labjs、yepnope、requirejs等。

如下,自己封装了一个简易loadjs文件——

var loadJS = function(url,callback){

    var head = document.getElementsByTagName('head')[0];

    var script = document.createElement('script');

    script.src = url;

    script.type = "text/javascript";

    head.appendChild( script);
    // script 标签,IE下有onreadystatechange事件, w3c标准有onload事件

    // IE9+也支持 W3C标准的onload

    var ua = navigator.userAgent,

        ua_version;

    // IE6/7/8

    if (/MSIE ([^;]+)/.test(ua)) {

        ua_version = parseFloat(RegExp["$1"], 10);

        if (ua_version <= 8) {

            script.onreadystatechange = function(){

                if (this.readyState == "loaded" ){

                    callback();

                }

            }

        } else {

            script.onload = function(){

                callback();

            };

        }

    } else {

        script.onload = function(){

            callback();

        };

    }

};

对于document.write的方式异步加载脚本,在这里就不说了,现在很少有人这么干了,因为浏览器差异性实在是搞得头大。

要注意,使用 Image 对象异步预加载 js 文件,里面的js代码将不会被执行。

最后,谈一下requirejs中的异步加载脚本。

requirejs不会保证按顺序运行目标脚本,只是保证它们的运行次序能满足各自的依赖性要求。从而我们确保了尽快的并行加载所有脚本,并有条不紊的按照依赖性拓扑结构去执行这些脚本。

 

四、总结

OK,谈到这儿,异步加载脚本的陈述也就完了。我再次??乱幌抡饫锏挠呕?承颉??/p>

1> 传统的方式,我们使用script标签直接嵌入到html文档中,这里分两种情况——

a> 嵌入到head标签中——要注意,这样做并不会影响文档内容中其他静态资源文件的并行加载,它影响的是,文档内容的渲染,即此时的DOM渲染就会被阻塞,呈现白屏。

b> 嵌入到body标签底部——为了免去白屏现象,我们优先进行DOM的渲染,再去执行脚本,但问题又来了。先说第一个问题——如果DOM文档内容比较大,交互事件绑定便有了延迟,体验便差了些。当然,我们需要根据需求而定,让重要的脚本优先执行。再说第二个问题——由于脚本文件至于body底部,导致对于这些脚本的加载相对于至于head中的脚本而言,它们的加载便有了延迟。所以,至于body底部,也并非是优化的终点。

c> 添加defer属性——我们希望脚本尽早的进行并行加载,我们把这批脚本依旧放入head中。脚本的加载应该与文档的加载同时进行,并且不影响DOM的渲染。这样,一旦文档就绪就可以运行脚本。所以便有了defer这样属性。但是要注意它的兼容性,对于不支持defer属性的浏览器,我们需要将代码封装在诸如jQuery之$(document).ready中。需要注意一点,所有的defer属性的脚本,是按照其出场顺序依次执行,因此,它同样严格同步。

 2> 上一点,讲的都是同步执行脚本(要注意,这些脚本的加载过程是并行的,只不过是谁先触发请求谁后触发请求的区别而已),接下来的优化点便是“并行执行脚本”,当然,我们知道,一个时间点,只有执行一个js文件,这里的“并行”是指,谁先加载完了,只要此时js引擎空闲,立马执行之。这里的优化分成两种——

a> 添加async这个属性——确实能够完成上面我们所说的优化点,但是它有很高的局限性,即仅仅是针对非依赖性脚本加载,最恰当的例子便是引入多个第三方脚本了。还有就是与deffer属性的合用,实在是让人大费脑筋。当然,它也存在兼容性问题。以上三个问题便导致其应用并不广泛。当使用async的时候,一定要严格注意依赖性问题。

b> 脚本加载脚本——很显然,我们使用之来达到“并行执行脚本”的目的。同时,我们也方便去控制脚本依赖的问题,我们便使用了如requirejs中对于js异步加载的智能化加载管理。

好,写到这儿。

这里,我仅仅谈的是异步加载脚本的相关内容。还有一块儿内容,便是异步加载样式文件或者其他静态资源。待续......

Javascript 相关文章推荐
超简单JS二级、多级联动的简单实例
Feb 18 Javascript
js实现简单选项卡与自动切换效果的方法
Apr 10 Javascript
JS/Jquery判断对象为空的方法
Jun 11 Javascript
js贪吃蛇网页版游戏特效代码分享(挑战十关)
Aug 24 Javascript
picLazyLoad 实现图片延时加载(包含背景图片)
Jul 21 Javascript
基于javascript实现最简单选项卡切换
Feb 01 Javascript
基于node下的http小爬虫的示例代码
Jan 11 Javascript
vue2.0实现前端星星评分功能组件实例代码
Feb 12 Javascript
vue element项目引入icon图标的方法
Jun 06 Javascript
JavaScript设计模式之建造者模式实例教程
Jul 02 Javascript
bootstrap动态调用select下拉框的实例代码
Aug 09 Javascript
Angularjs 根据一个select的值去设置另一个select的值方法
Aug 13 Javascript
JavaScript设计模式之工厂方法模式介绍
Dec 28 #Javascript
JavaScript设计模式之抽象工厂模式介绍
Dec 28 #Javascript
JavaScript设计模式之单件模式介绍
Dec 28 #Javascript
理解javascript回调函数
Dec 28 #Javascript
JavaScript设计模式之建造者模式介绍
Dec 28 #Javascript
如何编写高质量JS代码
Dec 28 #Javascript
JavaScript设计模式之原型模式(Object.create与prototype)介绍
Dec 28 #Javascript
You might like
php 引用(&amp;)详解
2009/11/20 PHP
laravel5.0在linux下解决.htaccess无效和去除index.php的问题
2019/10/16 PHP
jquery UI 1.72 之datepicker
2009/12/29 Javascript
详谈JavaScript内存泄漏
2014/11/14 Javascript
AngularJS实现表单手动验证和表单自动验证
2015/12/09 Javascript
Javascript将双字节字符转换成单字节字符并计算长度
2016/06/22 Javascript
JavaScript制作简易计算器(不用eval)
2017/02/05 Javascript
一次围绕setTimeout的前端面试经验分享
2017/06/15 Javascript
JavaScript之浏览器对象_动力节点Java学院整理
2017/07/03 Javascript
Angular4.0中引入laydate.js日期插件的方法教程
2017/12/25 Javascript
基于Swiper实现移动端页面图片轮播效果
2017/12/28 Javascript
在Vue项目中使用d3.js的实例代码
2018/05/01 Javascript
解决layui前端框架 form表单,table表等内置控件不显示的问题
2018/08/19 Javascript
在小程序Canvas中使用measureText的方法示例
2018/10/19 Javascript
Vue CLI3创建项目部署到Tomcat 使用ngrok映射到外网
2019/05/16 Javascript
JS根据json数组多个字段排序及json数组常用操作
2019/06/06 Javascript
Jquery Fade用法详解
2020/11/06 jQuery
python脚本实现统计日志文件中的ip访问次数代码分享
2014/08/06 Python
Python 基础教程之闭包的使用方法
2017/09/29 Python
在NumPy中创建空数组/矩阵的方法
2018/06/15 Python
Python中一般处理中文的几种方法
2019/03/06 Python
详解pytorch中squeeze()和unsqueeze()函数介绍
2020/09/03 Python
pandas按条件筛选数据的实现
2021/02/20 Python
8款使用 CSS3 实现超炫的 Loading(加载)的动画效果
2015/03/17 HTML / CSS
css3实现椭圆轨迹旋转的示例代码
2018/10/29 HTML / CSS
聪明的粉丝购买门票的地方:TickPick
2018/03/09 全球购物
怀旧香味蜡烛:Homesick
2019/11/02 全球购物
英国床垫和床架购物网站:Bedman
2019/11/04 全球购物
abstract 可以和 virtual 一起使用吗?可以和 override 一起使用吗?
2012/10/15 面试题
社会实践活动总结范文
2014/07/03 职场文书
交警正风肃纪剖析材料
2014/10/29 职场文书
投标邀请书范本
2015/02/02 职场文书
装配车间主任岗位职责
2015/04/08 职场文书
小英雄雨来观后感
2015/06/09 职场文书
2016年端午节校园广播稿
2015/12/18 职场文书
go语言中fallthrough的用法说明
2021/05/06 Golang