企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] # js的并行加载与顺序执行   javaScript文件(下面简称脚本文件)需要被HTML文件引用才能在浏览器中运行。在HTML文件中可以通过不同的方式来引用脚本文件,我们需要关注的是,这些方式的具体实现和这些方式可能会带来的性能问题。   当浏览器遇到(内嵌)`<script>`标签时,当前浏览器无从获知javaScript是否会修改页面内容。因此,这时浏览器会停止处理页面,先执行javaScript代码,然后再继续解析和渲染页面。同样的情况也发生在使用 `src` 属性加在javaScript的过程中(即外链 javaScript),浏览器必须先花时间下载外链文件中的代码,然后解析并执行它。在这个过程中,页面渲染和用户交互完全被阻塞了。   也就是说:每当浏览器解析到`<script>`标签(无论内嵌还是外链)时,浏览器会(一根筋地)优先下载、解析并执行该标签中的javaScript代码,而阻塞了其后所有页面内容的下载和渲染。 ## 五种引用脚本的方式: 1. Script DOM Element。 动态插入`<script>`,不会阻塞,但无法保持执行顺序。但唯有Firefox可以保持执行顺序,但也差点在Firefox 4 nightly的版本中去掉这个特性。 这种技术的重点在于: > 无论在何时启动下载,文件的下载和执行过程不会阻塞页面其他进程(包括脚本加载)。 然而这种方法也是有缺陷的。这种方法加载的脚本会在下载完成后立即执行,那么意味着多个脚本之间的运行顺序是无法保证的(除了Firefox和Opera)。当某个脚本对另一个脚本有依赖关系时,就很可能发生错误了。比如,写一个jQuery代码,需要引入jQuery库,然而你写的jQuery代码文件很可能会先完成下载并立即执行,这时浏览器会报错——‘`jQuery未定义`’之类的,因为此时jQuery库还未下载完成。于是做出以下改进: ~~~ function loadScript(url,callback){ var script=document.createElement(‘script’); script.type=”text/javaScript”; if(script.readyState){//IE script.onreadystatechange=function(){ if(script.readyState==”loaded”||script.readyState==”complete”){ script.onreadystatechange=null; callback(); } }; }else{//其他浏览器 script.onload=function(){ callback(); }; } script.src=url; document.getElementsByTagName(‘head’)[0].appendChild(script); } ~~~ 上述代码改进的地方就是增加了一个回调函数,该函数会在相应脚本文件加载完成后被调用。这样便可以实现顺序加载了,写法如下(假设file2依赖file1,file1和file3相互独立): `loadScript(‘file1.js’,function(){ loadScript(‘file2.js’,function(){}); }); loadScript(‘file3.js’,function(){}); ` file2会在file1加载完后才开始加载,保证了在file2执行前file1已经准备妥当。而file1和file3是并行下载的,互不影响。 虽然loadScript函数已经足够好,但还是有些不尽人意的地方——通过分析这段代码,我们知道,loadScript函数中的顺序加载是以脚本的阻塞加载来实现的(正如上述红字部分指出的那样)。而我们真正想实现的是——脚本同步下载并按相应顺序执行,即并行加载并顺序执行。 2. HTML5 async 非阻塞,加载完后立即执行,不保证顺序。这个属性不管有没有值、值为true或false,都是等同的效果(由于Kyle的推进,不能保证执行顺序与其值无关了)。 Google Analytics的新版嵌入代码就结合使用了上面两个方案,如: ~~~ var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); ~~~ 3. IE partsandspares.co.za defer属性。不阻塞,可以保证顺序,在DOM加载完成后执行(在`DOMContentLoaded`之前)。 4. `<script>`的type属性设为”script/cache” 非标准的type属性,使js文件只会被加载而不会执行。需要执行时,创建一个type属性为”text/JavaScript”的正常`<script>`元素,src设为前面已经加载的js地址即可,执行顺序开发者可控(执行时机也完全可控)。类似的方式也有通过`<img>`来做预加载的。 5. `document.write`。文档流关闭后执行会清空整个页面。 6. `XHR` 并行加载,执行顺序可控,但有同域限制。 ## 基本需求: Steve Souders 和 Nicholas C. Zakas 一起总结了下,认为js加载方案必须解决以下问题: * 支持特性检测 * 不会重复加载 * 支持并行加载 ## 解决方案: head.js LABjs LABjs库能帮我们真正地实现“并行加载与顺序执行”,推荐写法如下: ~~~ <script src="LAB.js"></script> <script type="text/javaScript"> $LAB .script("script1.js").wait() .script("script2-a.js") .script("script2-b.js") .wait(function(){ initScript1(); initScript2(); }) .script("script3.js") .wait(function(){ initScript3(); }); </script> ~~~ requireJS ~~~ <script src="require.js"></script>   <script type="text/javaScript">     require([       "script1.js",       "script2-a.js",       "script2-b.js",       "script3.js"      ],      function(){       initScript1();       initScript2();       initScript3();      }     );   </script> ~~~ ## 附录知识 ### 预编译期与执行期 JS是按照代码块来进行编译和执行的,**代码块间相互独立,但变量和方法共享**。什么意思呢? 举个例子,你就明白了: ~~~ <script type="text/javascript"> alert(str);//因为没有定义str,所以浏览器会出错,下面的不能运行 alert("我是代码块一");//没有运行到这里 var test = "我是代码块一变量"; </script> <script type="text/javascript"> alert("我是代码块二"); //这里有运行到 alert(test); //弹出"我是代码块一变量" </script> ~~~ 上面的代码中代码块一中运行报错,但不影响代码块二的执行,这就是代码块间的独立性,而代码块二中能调用到代码一中的变量,则是块间共享性。 JS的解析过程分为两个阶段:**预编译期(预处理)与执行期**。   预编译期 JS会对本代码块(两个script块互不影响)中的所有var声明的变量和函数进行处理(类似与C语言的编译)   此时处理函数的只是声明式函数,而且变量也只是进行了声明但未进行初始化以及赋值。   执行期 会按照代码块的顺序逐行执行 `代码块`: ~~~ <script type="text/javascript"> alert("first"); function Fn(){ alert("third"); } </script> <script type="text/javascript"> alert("second"); </script> ~~~ 可以总结出js执行的顺序: ~~~ step 1. 读入第一个代码块。 step 2. 做语法分析,有错则报语法错误(比如括号不匹配等),并跳转到 step 5。 step 3. 对 var 变量和 function 定义做“预编译处理”(永远不会报错的,因为只解析正确的声明)。 step 4. 执行代码段,有错则报错(比如变量未定义)。 step 5. 如果还有下一个代码段,则读入下一个代码段,重复 step 2。 step 6. 结束。 ~~~ `加载顺序测试代码:` 根据html文档流的执行顺序, 需要在页面元素**渲染前**执行的js代码应该放在`<body>`前面的`<script>`代码块中, 需要在页面**加载完成后**执行的js放在`</body>`元素后边, `body`的**onload事件是最后执行**的。 ![](![](images/screenshot_1515228812431.png)