如何异步加载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)
⚠️
async
和defer
这两个属性仅仅适用于外部属性,即脚本指定了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的共同点:
- 不会阻塞文档元素的加载。
- 使用这两个属性的脚本中不能调用document.write方法。
- 允许不定义属性值,仅仅使用属性名。
- 只适用于外部脚本(虽然IE4-IE7还支持对嵌入脚本的defer属性,但在IE8及之后的版本就只支持外部脚本,对不支持的会直接忽略defer属性,因此把延迟脚本放在页面底部仍然是最佳选择)
defer和async的不同点:
- 每一个async属性的脚本一旦加载完毕就会立刻执行,一定会在window.onload之前执行,但可能在document的DOMContentLoaded之前或之后执行。不保证按照指定它们的顺序来执行,如果JS有依赖性就要注意了。指定异步脚本的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容,因此,async适用于脚本对DOM无依赖。建议异步脚本不要在加载期间修改DOM。
- 每一个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在等待,填充的功能也在等待。所以async
和defer
也可以防止这一点。
2.load
浏览器加载了所有资源之后。
3.beforeunload/unload
用户离开页面时候触发。
unload和beforeunload的区别是,一个是已经卸载了,一个是在卸载之前。所以如果有一些确定的东西,大多在beforeunload
中处理。
document.readyState
有些时候需要知道页面执行到了哪个阶段,去执行相应的脚本。就可以通过readyState来知晓。readyState有三个阶段:
- loading => dom加载中
- interactive => dom解析完成,几乎与DOMContentLoaded同时发生,但是在DOMContentLoaded之前
- complete => 全部资源加载完毕,window.load.
readyState
有个对应的readystatechange
事件,每次readyState
变化的时候,都会调用readystatechange
本文固定连接:https://code.zuifengyun.com/2020/03/2060.html,转载须征得作者授权。