今天做一个网站发现js被阻塞了,无法实现异步加载,?js也要用异步加载不是引入了就行了,也没见速度变慢啊。后来仔细查询了一下,才发现自己原来还是肤浅了。
在说之前我先了解下面知识点:
defer和async是script标签的两个属性,用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。
在介绍他们之前,我们有必要先了解一下页面的加载和渲染过程:
1. 浏览器通过HTTP协议请求服务器,获取HMTL文档并开始从上到下解析,构建DOM;
2. 在构建DOM过程中,如果遇到外联的样式声明和脚本声明,则暂停文档解析,创建新的网络连接,并开始下载样式文件和脚本文件;
3. 样式文件下载完成后,构建CSSDOM;脚本文件下载完成后,解释并执行,然后继续解析文档构建DOM
4. 完成文档解析后,将DOM和CSSDOM进行关联和映射,最后将视图渲染到浏览器窗口
在这个过程中,脚本文件的下载和执行是与文档解析同步进行,也就是说,它会阻塞文档的解析,如果控制得不好,在用户体验上就会造成一定程度的影响。
所以我们需要清楚的了解和使用defer和async来控制外部脚本的执行。
在开发中我们可以在script中声明两个不太常见的属性:defer和async,下面分别解释了他们的用法:
defer:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
async:HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
如图所示,我们创建一个app
目录,用于放置一些简单的Web资源,同时创建了一个简易的Node服务器server.js
,其代码如下:
var http = require('http'); var fs = require('fs'); var typeMapping = { 'html': 'text/html', 'js' : 'text/javascript', 'css' : 'text/css', 'ico' : 'image/x-icon' }; var getResourceExtension = function(req) { var url = req.url; var lastIndexOfDot = url.lastIndexOf('.'); if (lastIndexOfDot === -1) return 'text/plain'; return url.substring(lastIndexOfDot + 1); }; var respondResourceToClient = function(req, res) { //read the reource and respond via 'pipe' fs.createReadStream(req.url.replace(/^\//, '')).pipe(res); }; var server = http.createServer(function(req, res) { console.log('requesting url: ', req.url); var extension = getResourceExtension(req); res.writeHead(200, {'Content-Type': typeMapping[extension]}); var delay = function(time) { setTimeout(function() { respondResourceToClient(req, res); }, time || 0); }; if (extension === 'html' || extension === 'css') { delay(0); } else if (extension === 'js') { delay(1000); } else { res.end(''); } }); server.listen(3000); console.log('listening at port 3000...');
从上面的代码我们可以看出,当服务器接收到请求之后,会判断请求资源是否为JS,如果是则延迟1s后返回对应的资源。
启动这个服务很简单,只需执行node server.js即可,然后就可以在浏览器中输入http://localhost:3000/app/index.html访问主页了,现在我们来看看index.html中的内容:
<!DOCTYPE html> <html> <head> <title>defer & async</title> <link rel="stylesheet" type="text/css" href="css/main.css"> <script type="text/javascript" src="js/1.js"></script> </head> <body> <div class="text">Hello World</div> <script type="text/javascript" src="js/2.js"></script> </body> </html>
在这个HTML文档中,我们先在head
中引用了一个外部的脚本文件js/1.js
,然后在我们要显示的Hello World后面又引用了一个js/2.js
,它们的内容都很简单,就是弹出对应的标示信息:
// js/1.js alert(1); // js/2.js alert(2);
下面我们就来访问主页,看看会发生些什么:
从图中可以看到,渲染的过程的确是自上而下,同步进行的,也就是说遇到外部的脚本,就得暂停文档的解析,下载并且解释执行,这种方式是阻塞的,会造成网页空白的现象。
现在稍微修改一下代码,将head
中的script
标签加上defer
属性,然后也稍微改动一下两个JS文件:
<!DOCTYPE html> <html> <head> <title>defer & async</title> <link rel="stylesheet" type="text/css" href="css/main.css"> <!-- adding a 'defer' attribute, by default, the value will be 'true' --> <script type="text/javascript" src="js/1.js" defer></script> </head> <body> <div class="text">Hello World</div> <script type="text/javascript" src="js/2.js"></script> </body> </html>
// js/1.js console.log(1); // js/2.js console.log(2);
再次访问index.html
,我们会在控制台中看到下面的执行顺序:
显而易见,1.js被延后致至文档解析完成后执行了,它的执行顺序比body中的<script>还要靠后。与默认的同步解析不同,defer下载外部脚本的不是阻塞的,浏览器会另外开启一个线程,进行网络连接下载,这个过程中,文档解析及构建DOM仍可以继续进行,不会出现因下载脚本而出现的页面空白。
关于defer我们需要注意下面几点:
1. defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer
2. 如果有多个声明了defer的脚本,则会按顺序下载和执行
3. defer脚本会在DOMContentLoaded和load事件之前执行
我们稍微改动一下代码,验证一下上面的几条:
<!DOCTYPE html> <html> <head> <title>defer & async</title> <link rel="stylesheet" type="text/css" href="css/main.css"> <!-- adding a 'defer' attribute, by default, the value will be 'true' --> <script type="text/javascript" src="js/1.js" defer></script> <script type="text/javascript" src="js/2.js" defer></script> <script type="text/javascript" defer> console.log(3); </script> </head> <body> <div class="text">Hello World</div> <script type="text/javascript"> document.addEventListener("DOMContentLoaded", function() { console.log('dom content loaded, ready state:', this.readyState); }, false); window.addEventListener('load', function() { console.log('window loaded, dom ready state:', document.readyState); }, false); </script> </body> </html>
上面代码中,head
中所有的script
标签都加上了defer
,其中第三个是内联脚本,然后我们也添加了DOMContentLoaded
和load
事件,下面来看一下打印的结果:
可以看到,外联的脚本是按照声明顺序执行的,内联脚本并没有遵守这个规则,另外,DOMContentLoaded和load事件依次被捕获,DOM的状态分别变为interactive和complete。
接下来我们介绍一下async属性,为了能够很好的演示执行顺序,我们还需要一些修改:
<!DOCTYPE html> <html> <head> <title>defer & async</title> <link rel="stylesheet" type="text/css" href="css/main.css"> <!-- adding a 'async' attribute, by default, the value is 'true' as well --> <script type="text/javascript" src="js/1.js" async></script> <script type="text/javascript" src="js/2.js" async></script> <script type="text/javascript" src="js/3.js" async></script> </head> <body> <div class="text">Hello World</div> </body> </html>
JS文件内如下:
// js/1.js console.log(1); // js/2.js console.log(2); // js/3.js console.log(3);
再次访问index.html
,会发现控制台打印如下:
我们发现,3个脚本的执行是没有顺序的,我们也无法预测每个脚本的下载和执行的时间和顺序。async和defer一样,不会阻塞当前文档的解析,它会异步地下载脚本,但和defer不同的是,async会在脚本下载完成后立即执行,如果项目中脚本之间存在依赖关系,不推荐使用async。
关于async,也需要注意以下几点:
1. 只适用于外联脚本,这一点和defer一致
2. 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序
3. async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序
下面这两张图可以更清楚地阐述defer和async的执行以及和DOMContentLoaded、load事件的关系
总结以下几点:
1,什么是异步加载js
使js文件脱离html解析的瀑布流加载,从而使js可以并行下载。
2,为什么要异步加载
一般写法将js放在head中,而且默认方式是同步加载,这就会导致在进行js加载的过程中,无法在其加载完成前对后续的内容进行操作,造成页面内阻塞,对用户体验很不友好。
3,如何异步加载
之前我们的写法就是把外部js或js放在body后或内部js放在body结束标签之前,这样会使至少先把一些基本内容加载出来而不会让人等太久,值得注意的是js中一般包含对dom的操作,如果放在body前,就可能出现空白或闪烁,而且js如果放在body内即结束标签之前,则无法获得onload和readystate,需要好好分析。
(1)、而比较常用的写法是动态的添加一个script标签,叫做Script DOM Element:
//立即执行函数 (function(){ //创建一个script标签 var scriptTag = document.creatElement("script"); //h4前type必须加上,h5可以不加 scriptTag.type = "text/javascript"; //h5新增的,用这种方法可以不写 scriptTag.async = true; //你的地址 scriptTag.src = "js地址"; //获取head标签 var headTag = document.getElementsByTagName("head")[0]; //在已有子节点前插入该标签,其实也可以用appendChild headTag.insertBefore(scriptTag, headTag.firstChild); })()
但是这个函数又放在哪里呢,我一时间没查到,不过想想应该是放在head中的,毕竟它也是同步加载的。但是这种方法有个问题,它会阻止onload函数的触发,但是我们有很多时候都需要使用这个函数来渲染一些其他东西,所以可以将该函数也放在onload中:
(function(){ //兼容ie,因为ie的window上没有addEventListener if(window.attachEvent){ window.attachEvent("load", asyncLoad); //非ie }else{ window.addEventListener("load", asyncLoad); } var asyncLoad = function(){ var scriptTag= document.createElement('script'); scriptTag.type = 'text/javascript'; scriptTag.async = true; scriptTag.src ="你的地址"; var headTag= document.getElementsByTagName('head')[0]; headTag.insertBefore(scriptTag, headTag); } )();
(2)、defer
该属性是h5新属性,只有ie可以用,主要可以延迟脚本的执行;
有3点需要注意:
1. defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer
2. 如果有多个声明了defer的脚本,则会按顺序下载和执行
3. defer脚本会在DOMContentLoaded和load事件之前执行
(3)、async
该属性声明外部js的异步加载;
也是3点:
1. 只适用于外联脚本,这一点和defer一致
2. 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序
3. async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序