浏览器中script标签加载顺序是如何的呢?这个问题折腾了好几次了,之前弄清楚了以后,觉得做不做笔记的无关紧要,可是后来发现,随着的时间的推移,知识逐渐被遗忘。然而更悲催的是,下次再遇到同样的问题的时候,脑中仅仅有一个模糊的印象,一切又得重新开始,关键是每一次又要花掉不少时间。废话不多,开始正题。
从浏览器加载HTML文件开始说起,如果HTML文件中引用了外部脚步文件,比如script标签,那么它是什么时候加载这个script标签的呢?如果有多个script标签,是不是它们是按顺序加载的呢?后面script标签的加载会不会等待前面script标签加载完毕后才开始呢?。。。。那defer和async又如何呢?下面一 一来探讨。首先,浏览器在加载HTML文件的时候,并不是边加载边解析,它是得到一份完整的HTML文件后才开始解析,而script标签是在HTML文件解析的时候才加载外部文件,理由很简单,因为只有在解析时浏览器才知道某个标签是做什么用的。有图有真相,看下图。
这是主文件的截图(index.jsp):
脚本文件的截图(one.jsp):
另一个脚本文件的截图(two.jsp):
最后一个脚本文件(websocket.jsp):
现在,向index.jsp文件发送一个请求,控制台输出是(chrome):
说明了什么?说明了script标签加载文件的时候是在HTML文件解析开始后。不知道有没有人发现,one.jsp文件并没有阻塞two.jsp和websocket.jsp文件的加载。这是因为,新版本的浏览器可以同时并发多个http请求,而我的浏览器时最新版本的。看到这里,可能有人会认为script标签引用外部文件的加载是按照它们在文档中出现的顺序加载的,起初我也是这么认为的,但事实似乎不是这样。再次向index.jsp文件发送一个请求,结果如下图:
上面红色的1和2分别表示第一次和第二次请求,结果说明,script标签并不是严格按照它们在文档中出现的顺序来加载外部文件的。但是,它们确实是按照在文档中出现的顺序来执行的:
上述情况在其它浏览器中的情况如何呢?先来看看IE(IE11):
我试了多次,每次结果都一样,上面是其中两次的截图,结果似乎表明,IE中是严格按照顺序加载的,并且前面的script标签加载过程中会阻塞后面script标签的加载。
再来看看在火狐中的结果:
我试了几次,这是前两次的结果,似乎也表明并不是按照顺序加载的,而且one.jsp文件也并没有阻塞后面websocket.jsp的加载。可是接下来几次的结果都是这样:
它们又按照顺序加载了,虽然前面的文件依然没有阻塞后面文件的加载。IE会一直阻塞后面文件的加载。前面提到,新版本的浏览器可以同时并发多个http请求(据前面的结果,这一点在IE中似乎不是),那么,浏览器可以同时并发多少个请求呢?我们来试试。修改一下index.jsp文件,其中script标签如下:
其中websocket标签的内容为:
其他标签的内容都类似这样:
下面是在chrome中截图:
我试了几次,每次都是谁一样,从中可以看出,chrome中最大并发数为6。
下面再来看看IE中的结果:
与预期一样,IE一如既往的阻塞了后面脚本的加载。再来看看火狐:
还是与预期一样,火狐与chrome的表现类似,一样是六个并发。
下面来看看defer如何影响脚本的加载。javascript高级程序设计(第三版)中指出,defer会指示脚本立即加载,但会延迟到页面解析完毕后在执行,并且defer指定的script标签不会再影响页面的解析,言外之意是defer指定的标签即使加载过程中发生阻塞也不会阻塞后面标签的解析。我们来看看效果。先修改index.jsp中的文件,如下图:
另外监听在index.jsp中DOMContentLoaded事件:
其他文件的内容不变,现在来发生一个请求试试:
结果真是出乎意料,再一次请求试试:
变化不大,从上面的结果分析,defer指示的 script标签似乎是等到其他标签加载完成后再加载,即使同时的http请求还没达到最大并发数,同时,前后两次请求中,defer指示的标签一直是按出现的先后顺序加载的。还有就是,defer指示的标签在DOMContentLoaded事件前执行。
先来验证一下defer指示的标签是不是一直按照先后顺序加载的,我们来修改下index.jsp文件:
其他地方不变,发送一个请求看看:
看来并不是这样,加载还是不严格按照script标签在文档中的顺序。
把index.jsp文件修改为原样:
发送一个请求,看看在火狐中的结果:
结果当真是出乎意料,第一与chrome‘分道扬镳’,再来试一次:
还是一样。再来看看在IE中的结果:
见鬼,IE难得的出现了并发并且出现了6个并发(5个defer一个非defer),并且defer推迟到正常标签执行完毕后执行。我们来看看IE最大并发数是多少,修改index.jsp文件如下:
发送一个请求看看:
同样是6,并且也不再按照script标签出现的顺序加载了。前面提到,IE中正常script标签会阻塞后面正常script标签的加载,现在来看看它是否也阻塞defer指示的script标签的加载。修改index.jsp文件:
发送一个请求看看:
还是会阻塞defer指示的script标签。再来修改index.jsp文件看看:
发送一个请求:
并发数是5,还是一样,即使没达到最大并发数,正常script标签一样阻塞后面标签的加载。
现在来看看async是如何影响script标签的加载与执行的,修改index.jsp文件:
在 chrome中发生 一个请求:
在chrome中倒是与defer一样,同时,有两点不一样,一点是,async指示的script标签的执行在DOMContentLoaded事件之后,另一点是,即使四个async标签加载完成的先后顺序是:four.jsp、three.jsp、one.jsp、two.jsp,但它们的执行顺序却是:one.jsp、two.jsp、four.jsp、three.jsp,几乎无序可寻。
再来看看在火狐中的结果:
和chrome的差别很大,async指示的四个文件加载完成的先后顺序是:four.jsp、two.jsp、one.jsp、three.jsp,而他们执行的先后顺序是:four.jsp、two.jsp、one.jsp、three.jsp。加载完成顺序与执行顺序一致。但是,他们完全没有等到正常script标签执行就开始执行了。为了结果更可靠一些,再来次请求:
async指示的四个文件加载完成的先后顺序是:four.jsp、two.jsp、one.jsp、three.jsp,而他们执行的先后顺序是:four.jsp、two.jsp、one.jsp、three.jsp,加载完成顺序与执行顺序一致,并且加载完成就执行。
最后来看看IE,发送一个请求:
从上述结果可以看出,IE和火狐对待async标签的方式差不多,不同的一点是,IE中async标签与chrome中一样,并不是按照下载完成顺序执行。再来看看IE会不会阻塞async标签的加载,修改index.jsp文件:
发送一个请求:
从结果可以看出,IE中正常标签同样阻塞了async标签的加载。async标签同样不按加载完成顺序执行。
总结一下,三个浏览器最大并发数为6。在chrome中先全部加载正常标签再加载defer和async标签(但正常标签加载的阻塞可能会阻塞其他标签的加载),同时,defer标签在DOMContentLoaded事件前执行,且按照script标签在文档中出现的顺序执行,async标签加载完毕就可能执行,不会等待正常标签的执行,无论是在DOMContentLoaded事件之前还是之后,且执行顺序与async标签加载完成先后顺序无关。除此之外,在chrome中在请求并发数没达到最大并发数时,所有标签都不会阻塞其他标签的加载。在火狐中,所有标签都是并发加载,正常标签不会阻塞defer标签和async标签的加载,但是defer标签会在正常标签执行完毕后再按在文档中出现的顺序执行。async标签则是加载完毕后就执行,而不会等待正常 script标签的执行,同时他们的加载顺序与他们在文档中出现的顺序也没关系。在IE中,正常标签加载的阻塞会阻塞其他在文档中出现在它后面的script标签的加载,无论是正常标签还是defer和async标签。defer和async标签加载的阻塞不会阻塞其他在文档中出现在它后面的script标签的加载,无论是正常标签还是defer和async标签。defer标签会推迟到正常标签执行完毕后再按在文档中出现的顺序执行,并且async标签的执行并不是按照其加载完成先后顺序,async标签的执行与chrome一样,无序可寻。无论是在火狐还是IE或者chrome中,defer和async标签都可以在DOMContentLoaded事件前执行,但只有async标签可以在DOMContentLoaded事件后执行。简单来讲,async标签加载完成后就可能执行且执行可能无序,而不会等待其他标签的执行,而defer则是加载完成后需要等待正常标签执行完毕后才会执行。defer标签和正常标签一定会在DOMContentLoaded事件之前执行完毕,而async标签可能会在DOMContentLoaded事件之前或之后执行。关于defer和async与DOMContentLoaded事件的关系,留待下一篇文章详细探讨。
下面的一段话摘取某篇博文:
Both async and defer scripts begin to download immediately without pausing the parser and both support an optional onload handler to address the common need to perform initialization which depends on the script. The difference between async and defer centers around when the script is executed. Each async script executes at the first opportunity after it is finished downloading and before the window’s load event. This means it’s possible (and likely) that async scripts are not executed in the order in which they occur in the page. The defer scripts, on the other hand, are guaranteed to be executed in the order they occur in the page. That execution starts after parsing is completely finished, but before the document’s DOMContentLoaded event.
摘自http://w3c.github.io/html/semantics-scripting.html#data-block:
The async and defer attributes are boolean attributes that indicate how the script should be executed.Classic scripts may specify defer or async; module scripts may specify async.
There are several possible modes that can be selected using these attributes, and depending on the script’s type.
For classic scripts, if the async attribute is present, then the classic script will be fetched in parallel to parsing and evaluated as soon as it is available (potentially before parsing completes). If the async attribute is not present but the defer attribute is present, then the classic script will be fetched in parallel and evaluated when the page has finished parsing. If neither attribute is present, then the script is fetched and evaluated immediately, blocking parsing until these are both complete.
For module scripts, if the async attribute is present, then the module script and all its dependencies will be fetched in parallel to parsing, and the module script will be evaluated as soon as it is available (potentially before parsing completes). Otherwise, the module script and its dependencies will be fetched in parallel to parsing and evaluated when the page has finished parsing. (The defer attribute has no effect on module scripts.)
classic script and module script:
The script element allows authors to include dynamic script and data blocks in their documents. The element does not represent content for the user.
The type attribute allows customization of the type of script represented :
Omitting the attribute, or setting it to a JavaScript MIME type, means that the script is a classic script, to be interpreted according to the JavaScript Script top-level production. Classic scripts are affected by the charset, async, and defer attributes. Authors should omit the attribute, instead of redundantly giving a JavaScript MIME type.
Setting the attribute to an ASCII case-insensitive match for the string "module" means that the script is a module script, to be interpreted according to the JavaScript Module top-level production. Module scripts are not affected by the charset and defer attributes.
Setting the attribute to any other value means that the script is a data block, which is not processed. None of the script attributes (except type itself) have any effect on data blocks. Authors must use a valid MIME type that is not a JavaScript MIME type to denote data blocks.