js外部脚本异步加载方式

如何异步加载js脚本

1.动态插入标签的方式

通过操作dom,可以在任意位置创建js脚本,这种方式优点是无论在何时启动下载,文件的下载和执行过程不会阻塞页面其他进程(包括脚本加载)。

var script=document.createElement('script'); 
script.type='text/javaScript'; 
script.src='file1.js'; 
document.getElementsByTagName('head')[0].appendChild(script); 

但缺陷是:这种方法加载的脚本会在下载完成后立即执行,那么意味着多个脚本之间的运行顺序是无法保证的。当某个脚本对另一个脚本有依赖关系时,就很可能发生错误了。我们可以增加一个回调函数,该函数会在相应脚本文件加载完成后被调用。这样便可以实现顺序加载了。

loadScript('file1.js',function(){ loadScript('file2.js',function(){}); }); 
loadScript('file3.js',function(){}); 

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.οnlοad = function() {
			callback();
		};
	}
	script.src = url;
	document.getElementsByTagName('head')[0].appendChild(script);
}

2.使用模块化加载

上述动态插入方式虽然能够实现异步加载,但若要实现顺序执行,必须依赖js的阻塞加载来实现顺序回调。而我们真正想实现的是——脚本异步下载并按相应顺序执行,即并行加载并顺序执行。

我们可以使用requireJS插件来实现模块化加载(AMD规范)。但需要引入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>

AMD的缺点是开始就把所有依赖写出来,这是不符合书写的逻辑顺序的。

3.使用js脚本异步加载方式async,defer

页面的生命周期主要有三个

1.DOMContentLoaded

这个是dom树构建完成之后,并没有进行加载资源(styles, imgs)。处理cssom之前的dom树完成的情况。

DOMContentLoaded and scripts

我们知道js加载会阻塞dom树的构建,因此可知道dom树构建完成的时候,js是已经解析完成了(正常情况)

非正常的情况,就是js脚本的加载方式(async, defer)

⚠️asyncdefer这两个属性仅仅适用于外部属性,即脚本指定了src属性,否则会被忽略。

在没有定义defer和async之前,异步加载的方式是动态创建script,通过window.onload方法确保页面加载完毕再将script标签插入到DOM中。

使用这两个属性,浏览器就知道会去继续解析dom,并且在后台加载执行这些js。

<script>标签的这两个属性(async, defer)还是有些不同的。

defer="defer"async="true/false"

async:HTML5新的异步、并行模式,脚本将在完成下载后等待合适的时机执行代码。

加载顺序: 谁先加载完谁就先执行。

DOMContentLoaded: 在dom未完全解析之前,在这个阶段就可以下载执行异步的脚本。如果脚本很小或缓存,并且文档足够长,就会发生这种情况

defer:告诉浏览器该脚本不会在页面加载完成之前操作DOM,脚本将会和其他资源文件并行下载。

加载顺序: defer的这个属性,始终是按照在dom中的顺序来加载执行。相当于window.onload,但它比window.onload更灵活

DOMContentLoaded: 他会延迟到在dom加载解析之后加载执行,但是在DOMContentLoaded事件之前。

html4.0中定义了defer;html5.0中定义了async。

(1)没有defer或async,浏览器会立即加载并执行指定的JS脚本,也就是说,不等待后续载入的文档元素,读到JS脚本就加载并执行。

(2)有async,加载后续文档元素的过程将和JS的加载与执行并行进行(异步)。

(3)有defer,加载后续文档元素的过程将和JS的加载并行进行(异步),但JS的执行要在所有文档元素解析完成之后,DOMContentLoaded 事件触发之前完成。

defer和async的共同点:

  1. 不会阻塞文档元素的加载。
  2. 使用这两个属性的脚本中不能调用document.write方法。
  3. 允许不定义属性值,仅仅使用属性名。
  4. 只适用于外部脚本(虽然IE4-IE7还支持对嵌入脚本的defer属性,但在IE8及之后的版本就只支持外部脚本,对不支持的会直接忽略defer属性,因此把延迟脚本放在页面底部仍然是最佳选择)

defer和async的不同点:

  1. 每一个async属性的脚本一旦加载完毕就会立刻执行,一定会在window.onload之前执行,但可能在document的DOMContentLoaded之前或之后执行。不保证按照指定它们的顺序来执行,如果JS有依赖性就要注意了。指定异步脚本的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容,因此,async适用于脚本对DOM无依赖。建议异步脚本不要在加载期间修改DOM。
  2. 每一个defer属性的脚本都是在文档元素完全载入后,一般会 的顺序执行,同时一般会在document的DOMContentLoaded之前执行,相当于window.onload,但应用上比 window.onload 更灵活!实际上,defer 更接近于DomContentLoad。常适用于脚本对DOM有依赖的情况。事实上,延迟脚本不一定会按顺序执行,也不一定会在DOMContentLoaded事件触发之前执行,因此最好只包含一个延迟脚本。

也就是说:defer是“渲染完再执行”,async是“下载完就执行”。故而,defer一般按原本顺序执行,而async的执行顺序被下载完成时间影响。

异步加载的脚本不一定按顺序执行,解决顺序问题办法:最好只有一个脚本。(解决多个脚本执行顺序问题最好就是合并js)。但简单合并js并不优雅,因为合并的js过大在加载时也会消耗性能,而且无法更好的分模块,不利于维护和不同页面的脚本区分。

现代(高版本)浏览器在html5和ES6的加持下,已经原生实现ES Module模块化加载,在script标签中使用type="module"属性可以原生实现ES Module

这里注意是ES6模块方法而不是nodejs的CommonJS方法(CommonJS模块是同步阻塞加载的,不适用于浏览器),CommonJS模块输出是值的拷贝(加载完成会有缓存),ES6模块输出是值的引用(引用时可能修改到模块的值)。CommonJS是运行时加载,ES6模块是编译时加载。

 // 方法 1 : 引入module.js,然后在script标签里面调用
  <script type="module">
    import test from './module.js';
    console.log(test())
  </script>
 
  // 方法 2 : 直接引入index.js,使用src引入
  <script type="module" async="true" src="./index.js"></script>

DOMContentLoaded and styles

我们知道,css不会直接的阻塞dom树的构建,但是css的解析执行会阻塞js的下载执行,从而间接的影响到dom的构建.

Built-in browser autofill

浏览器内置的填充功能,比如有些网站的登录的账号密码,会在DOMContentLoaded时自动填充进去,用户允许的情况。

因此脚本长时间加载执行,会导致DOMContentLoaded在等待,填充的功能也在等待。所以asyncdefer也可以防止这一点。

2.load

浏览器加载了所有资源之后。

3.beforeunload/unload

用户离开页面时候触发。

unload和beforeunload的区别是,一个是已经卸载了,一个是在卸载之前。所以如果有一些确定的东西,大多在beforeunload中处理。

document.readyState

有些时候需要知道页面执行到了哪个阶段,去执行相应的脚本。就可以通过readyState来知晓。readyState有三个阶段:

  1. loading => dom加载中
  2. interactive => dom解析完成,几乎与DOMContentLoaded同时发生,但是在DOMContentLoaded之前
  3. complete => 全部资源加载完毕,window.load.

readyState有个对应的readystatechange事件,每次readyState变化的时候,都会调用readystatechange

加载中...
加载中...