JavaScript 异步加载JS
异步加载JS
js加载的缺点:加载工具方法没必要阻塞文档,过多js加载会影响页面效率,一旦网速不好,那么整个网站将等待js加载而不进行后续渲染等工作。
有些工具方法需要按需加载,用到再加载,不用不加载。
一、为什么要异步加载JS
1、JS文件是怎么加载的?
JS文件是同步加载的,
加载到js文件的时候就卡在那了,阻断HTML和CSS的加载线,等js文件加载完并且执行完之后,HTML和CSS再继续下载
比如引入 tools.js 文件,文件内容是 alert( '阻断HTML和CSS的加载线,看不到H1标签' );
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> <script type="text/javascript" src="./tools.js"></script> </head> <body> <h1>文章标题</h1> </body> </html>
2、为什么 JS 能阻断 HTML 和 CSS 的加载线,为什么 JS 的下载过程 和 执行过程,不能和 HTML、CSS 并行呢?
因为 JS 会修改 HTML 和 CSS,
这头 HTML 和 CSS 动态的加载还没有绘制,JS 给修改了这是不行的,
所以要么绘制完页面等 JS 修改,要么等 JS 执行完再继续下载,反正不能同时下载这是根本原则。
但是有些时候的需求是,想让 JS 变成异步加载的
为什么这么说呢?
JS正常来说是做 DOM 修改的,但是有一些 JS 文件的作用是初始化数据的,跟页面根本就没关系不会操作页面,
而有些 JS 是引入工具包的,工具包是一个一个 function,不调用它根本就不会执行,它也不会影响页面,而这些我们希望像工具一样并行的加载下来。
3、为什么希望并行下载下来?
如果所有 JS 全是这种同步的,全是这种阻塞后续页面的,如果JS包过多十多个JS包,并且这十多个JS包并不都是处理页面的,有些JS包就是作为辅助的,
那么这么多JS包,但凡有一个包出现1k,叫一个数据量的误差没下载下来,网络阻塞了整个页面就废掉了,有一个字节没加载下来,后面的页面都加载不了了要等着,因为JS有阻塞后续页面的作用。
比如有时候手机访问一个页面,一开始都会留白,留一段时间后才会展示页面,留白时加载的全是JS,
js页面没有下载下来,后续页面别想下载下来,要等js页面完事后,后续页面一边下载HTML一边下载CSS进行绘制。
有如果 js 写的过多了,写了十多个 js 文件,风险概率越大,但凡有一个文件出现一丁点毛病,后面的页面就别想下载了,
能不能把那些无关的,不修改页面的JS换成一种异步的加载,同时的加载,一边下载 JS 一边下载 HTML 和 CSS,能不能办到呢?
4、javascript异步加载有三种方法
1. defer 异步加载,但要等到dom文档全部解析完才会被执行。只有IE能用。
2. async 异步加载,加载完就执行,async只能加载外部脚本,不能把js写在script标签里。
1和2 执行时也不阻塞页面
3. 创建script,插入到DOM中,加载完毕后callBack
二、第一种方法 defer
单纯这么写 js 还是同步加载
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>加载js</title> <script type="text/javascript" src="tools.js"></script> </head> <body> </body> </html>
想让js变成异步加载的方式很简单,在 script 的头标签上加 defer
1. 从此之后这个js就变成异步加载的js了,
系统读到这里不会阻断 html 和 css 的下载,js会和html、css并行的下载
2. defer有一个小问题,只有IE9以下可以用
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>异步加载js</title> <script type="text/javascript" src="tools.js" defer="defer"></script> </head> <body> </body> </html>
凡是这种 defer="defer" 属性名等于属性值的,写一个属性名 defer 就行了
<script type="text/javascript" src="tools.js" defer></script>
defer 除了引入外部的文件异步的引入以外,还可以把代码写到内部
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>异步加载js</title> <script type="text/javascript" defer> // 这块script代码,也变成异步下载的了 var a = 123; </script> </head> <body> </body> </html>
2、异步加载的defer什么时候执行?
defer的执行时刻是要等到,整个文档全部解析完才会被执行
正常的js标签下载完立即执行,执行完后才会加载HTML和CSS,
而 defer 这种js标签不是下载完立即执行,defer 要等到整个页面全部解析完才会执行。
什么是整个全部页面解析完呢?
也就是说 DOM 树生成完,也就是整个浏览器把标签从第一行扫到最后一行,把DOM树构建完了,叫整个页面的解析完毕。
解析完毕一定发生在页面加载完毕之前,
因为可能还有一些图片文件或文字还没下载完,所以defer的执行时刻发生在整个页面解析完毕时。
PS
解析完:DOM树加载完
加载完:图片、文字、音频、视频...,都下载完
三、第二种方法 async
async 的功能和 defer 是基本类似的,也可以实现script标签的异步加载,
只不过 async 是W3C标准方法,IE9以上都可以用,chrome、firefox、opera都可以用是一个标准方法
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>async异步加载js</title> <script type="text/javascript" src="tools.js" async="async"></script> </head> <body> </body> </html>
async有一些注意点:
1. 第一个,W3C是标准方法
2. 第二个,async不像defer一样
defer 要等到整个文档全部解析完毕才执行,
async 是加载完立马就执行并且是异步的,执行也是异步的,它不会影响页面里的其它东西
3. 还有一条是 async 只能加载外部脚本,也就是说不能在 script 里面写代码,只能在里面写 src 等于一个外部脚本
async的简单记忆法
asychronous 是异步的意思,把这个单词记住了 async 就好记了,async 是异步这个词的一个缩写(asychronous再加几个单词javascript、and、xml就是ajax的缩写)
总结
1、现在实现异步脚本的加载有两种方法
第一种: defer 方法,是IE用的,
第二种: async 方法,是标准浏览器用的,当然IE的高版本浏览器也是可以用的
defer 要等整个文档解析完毕,才会执行它加载完的脚本
async 加载完脚本就立马执行
这两个在执行脚本的时候都是异步的,都不用影响页面其它部分的加载(不会去阻塞的)
2、有一个区别
defer 里面除了可以引入外部的js文件,让外部的js文件变成异步的以外,还可以让内部的js文本变成异步的,也就是说可以把代码写在js标签里面
async 只能加载外部的js文件,这是一个典型的区别
<script type="text/javascript" defer="defer"> // ... </script>
async与defer的选择
https://zhuanlan.zhihu.com/p/637269351
3、现在有一个问题
兼容性不好搞定,想让任何浏览器都能实现异步脚本的加载怎么办?
这两一起写就崩溃了,问题这两个一起写也不行啊,在IE9以上的浏览器又能识别 defer 又能识别 async,它两就冲突了吗!
<script type="text/javascript" src="tools.js" defer="defer" async="async"></script>
另一个方法,
再来一个script标签,一个写async一个写defer加载同样的脚本
<script type="text/javascript" src="tools.js" async="async"></script> <script type="text/javascript" src="tools.js" defer="defer"></script>
理论上没毛病,但从代码上加载两次同样的脚本,冲不冲突先不说,可能有问题,
因为async上面的脚本加载完了之后会异步执行,
下面defaer的脚本会加载完后会等到解析完才会执行,
他两谁先执行不一定,而且这两脚本加载完后,代码会重叠、会发生覆盖,
有可能第一个脚本在执行代码的时候把值赋完了,第二个脚本拿到原来的值再处理就发生错误了,
因为它两执行的时刻都不一样,代码覆盖代码重叠产生执行顺序的冲突,这样也是不行的。
那怎么办呢?就引出了第三种方法
四、第三种方法,创建script,插入到DOM中,加载完毕后callBack
第三种方法就比较高端了,通杀所有浏览器,而且也是最常用的方法
1. 非常强大除了可以实现异步加载以外
2. 还可以按需加载,我什么候需要这个脚本,什么时候加载过来
1、什么是按需加载呢?
比如说页面上有一个按钮,
这个按键点击完之后,会生成一大堆新的东西,但是经过统计用户只有 0.01% 的概率会按这个按钮,
那这个按钮点击完之后,需要的方法,需要执行的东西可以放到一个 js 文件里面,当用户去点击的时候,动态的下载完这个 js 文件,然后再去执行。
因为有太大的概率不需要这些代码,没必要让它加载到页面占内存空间,
所以有些时候是按需加载,需要的时候再加载过来,总之一切的情况都是为了优化效率。
2、下面是今天的重点
1. 在页面里动态的生成一个 dom 结构,还可以创建一个 script 标签
2. 创建完后,给 script 标签一 type 属性与值(当然写不写是无所谓的,type值是无所谓的)
3. 接下来给script标签加属性 scr = "tools.js"
scr = "tools.js" 这句执行完系统就会下载tools.js地址里面的东西了,就是开启一个线程加载srcipt标签了,而且下载的过程中也是异步的去下载
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>第三种方法</title> </head> <body> <script> var script = document.createElement('script'); // 1.创建一个script标签 script.type = "text/javascript"; // 2. 创建完后给一个type值 script.src = "tools.js"; // 3.接下来给script标签加一个src属性,值等于"tools.js" // 写到第3步的时候 // 让src等于值的时候,script.src = "tools.js"这句执行完, // 系统就会下载tools.js地址里面的东西了,就是开启一个线程加载srcipt标签了,而且下载的过程中也是异步的去下载 // 但是下载了完了,什么时候执行呢? // 如果代码只写了这么三步,它永远不会执行。 </script> </body> </html>
但是下载了完了,什么时候执行呢?
如果代码只写了这么多他永远不会执行,
那什么时候执行呢?
4. 当把创建的script标签插入到页面里面去的时候执行
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>第三种方法</title> </head> <body> <script> var script = document.createElement('script'); // 1.创建一个script标签 script.type = "text/javascript"; // 2. 创建完后给一个type值 script.src = "tools.js"; // 3.接下来给script标签加一个src属性,src属性值等于"demo.js" document.head.appendChild(script); // 4. 当把标签插入到页面里面去的时候,才会解析这个脚本,否则只是下载完什么都不干 </script> </body> </html>
3、写一个例子
写一个tools.js文件,文件内容是 alert('异步加载');
把最后一行注释掉,保存刷新会执行吗?
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>第三种方法</title> </head> <body> <script> var script = document.createElement('script'); script.type = "text/javascript"; script.src = "tools.js"; // document.head.appendChild(script); // 把这行注释掉 </script> </body> </html>
保存刷新页面不会执行
但是tools.js下载了吗?
点击Network,Network会把所有下载多的东西展示到这里面去,所有的网络请求都放在这
有点遗憾没有显示,但确实是下载了,理解一下有,肯定下载了。
为什么这个肯定呢?
因为有个灯塔模式,灯塔模式是创建一个img标签,
然后让img标签只作为一个预加载的层面,不去加载到页面里面去,
让src里面的值赋过来之后,形成一个预加载,以后用的时候方便,
不用二次加载,去拿缓存的就可以了,这是一个预加载机制。
这块script.src也会把tools.js下载过来的,当document.head.appendChild(script) 把script签添加到页面里面去的时候它才会去执行
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>第三种方法</title> </head> <body> <script> var script = document.createElement('script'); script.type = "text/javascript"; script.src = "tools.js"; document.head.appendChild(script); // 添加js文件到页面上 </script> </body> </html>
这样的过程实现了一个异步加载js的过程
1). 新创建一个srcipt标签
2). 让srcipt.src等于tools.js这个过程,就是一个异步加载的过程
3). 然后把srcipt添加到页面里面去,就形成了一个异步加载的script标签了
4、现在有个问题
加载的tools.js文件里面有个test方法
function test(){ console.log('a'); }
现在要执行test()能执行吗?
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>第三种方法</title> </head> <body> <script> var script = document.createElement('script'); script.type = "text/javascript"; script.src = "tools.js"; document.head.appendChild(script); test(); // 执行test方法 </script> </body> </html>
不能执行
为什么不能执行呢?
也不是真的不能执行,加一个定时器,在一秒之后再执行
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>第三种方法</title> </head> <body> <script> var script = document.createElement('script'); script.type = "text/javascript"; script.src = "tools.js"; document.head.appendChild(script); setTimeout(function(){ test(); }, 1000); </script> </body> </html>
就可以执行了
为什么一秒之后能执行,当前执行不了呢?
因为还没下载完,程序执行是以微秒计时的,
1). 下载demo.js需要发一个请求,等请求响应完后,回归这个资源,
有一个发请求、回归资源的一个过程。
2). 在发生这个过程中,系统就把script.src = "demo.js"下面的代码就执行完了,
因为程序的执行是非常快的,并且这个下载还是异步下载的没有阻塞
3). 所以当程序执行 document.head.appendChild(script); 把script标签添加到页面里面去的时候,demo.js还没下载完呢,
当程序执行test()的时候,demo.js可能还没下载完呢
问题来了,我引入的是工具方法,引入之后无疑是想让test()执行,
现在什么时候执行成问题了,什么时候下载完能用,不能等秒数!
能不能有一个提示我们的机制demo.js下载完了,得到提示下载完了之后再调用他的test()方法?
5、load事件
有一个机制叫load是一个事件,之前说过window上面有一个load事件(window.noload),
没说只有window上有,load不只window有,但凡能下载的都有load事件。
script.onload onload事件代表当触发load事件的时候就代表下载完了
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>第三种方法</title> </head> <body> <script> var script = document.createElement('script'); script.type = "text/javascript"; script.src = "tools.js"; script.onload = function(){ test(); } document.head.appendChild(script); </script> </body> </html>
当下载完了我们再调用test()就能执行,每次刷新都能打印出a
这就确保了下完之后再去执行test()方法,如果下载不完就永远不去执行,他的兼容性非常好标准浏览器都兼容,就IE不兼容,
IE就script标签上没有load事件,IE有自己的语法,提供了一套完整方法。
6、IE有一个状态码readyState
IE非常特殊有一个状态码readyState,
谁上面的状态码呢?是script标签上的状态码。
状态码就是一个属性,这个属性里面存值了,
一开始值是loading,readyState会根据script标签加载的进度,去动态改变里面的值,
如果script标签加载完,readyStaue的值会改成complete
script.readyState = "loading" 一开始是loading
script.readyState = "complete" 如果标签加载完了会改成complete
script.readyState = "loaded" 或者改成loaded
当值变成loading的时候代表加载完了,然后监听readyState,
1). IE里面提供了一个事件onreadystatechange
2). 当readyState发生改变的时候,会触发这个onreadystatechange事件
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>第三种方法</title> </head> <body> <script> var script = document.createElement('script'); script.type = "text/javascript"; script.src = "tools.js"; script.onreadystatechange = function(){ // 这个事件监听状态码什么时候变,ajax时还会在接触这个事件 // 这里面改变一次会触发一次 // script.readyState等于"conplete" 或者 script.readyState等于"loaded"代表加载成功了 if(script.readyState == "complete" || script.readyState == "loaded"){ test(); } } document.head.appendChild(script); </script> </body> </html>
把IE的和非IE的两个方法和在一起
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>第三种方法</title> </head> <body> <script> var script = document.createElement('script'); script.type = "text/javascript"; script.src = "tools.js"; if(script.readyState){ // 有readyState用IE script.onreadystatechange = function(){ if(script.readyState == "complete" || script.readyState == "loaded"){ console.log('IE浏览器,下载成功'); test(); } } }else{ script.onload = function(){ console.log('chrome浏览器,下载成功'); test(); } } document.head.appendChild(script); </script> </body> </html>
五、封装成一个loadScript函数,当需要异步加载一个script标签的时候用这函数
函数里面传两个参数
1). 第一个参数,每次加载的js文件都不一样,把src的值变成参数url等待用户传
2). 还有一个参数是什么?
按需加载script标签,是为了执行一个函数,每次执行的函数都是未知,把这个函数传进去,我们把绑定的事件处理函数叫回调函数,
为什么叫回调函数?
当满足一定条件才执行的函数叫回调函数,这块也是当满足一定条件才执行它也叫回调函数,回调函数的名字叫callback
下面是封装的函数
function loadScript(url, callback){ var script = document.createElement('script'); script.type = "text/javascript"; script.src = url; // 第一个参数 if(script.readyState){ script.onreadystatechange = function(){ if(script.readyState == "complete" || script.readyState == "loaded"){ callback(); // 第二个参数回调函数 } } }else{ script.onload = function(){ callback(); // 第二个参数回调函数 } } document.head.appendChild(script); }
函数还有点小问题
1). onreadystatechange监听的状态码是readyState的变化
2). 先发生 script.src = url ,然后在绑定事件函数 script.onreadystatechange = function(){ ... }
3). 有没有一种情况,很大的光纤,比本机存储还要快,script.src = url 这行瞬间就把资源下载完了,readyState瞬间就变到最终complete状态了
4). 瞬间就到了complete状态,上面 script.onreadystatechange = function(){ ... } 绑定的事件函数还有意义吗?
在绑定之前已经是complete瞬间到终止状态了,绑定的事件永远不会触发了
事件触发依赖于从loading变成complete这样一个过程,在绑定事件之前就完事了绑定还有什么用?
解决方法是把 script.src = url 这句放到,绑定事件之后,
1). 意思是先执行script.onreadystatechange = function(){ ... } 绑定事件,事件先帮上
2). 然后在 script.src = url 加载文件
function loadScript(url, callback){ var script = document.createElement('script'); script.type = "text/javascript"; // script.src = url; // 2. 他先发生然后在绑定函数 if(script.readyState){ script.onreadystatechange = function(){ // 1. onreadystatechange监听的是状态码的变化 if(script.readyState == "complete" || script.readyState == "loaded"){ callback(); } } }else{ script.onload = function(){ callback(); } } script.src = url; // 3.把这行放到绑定事件之后,先执行上面的绑定事件,再加载文件 document.head.appendChild(script); }
下面使用一下封装的按需加载函数,为什么会报错呢?
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>loadScript</title> </head> <body> <script> loadScript('demo.js', text); // 报错 function loadScript(url, callback){ var script = document.createElement('script'); script.type = "text/javascript"; if(script.readyState){ script.onreadystatechange = function(){ if(script.readyState == "complete" || script.readyState == "loaded"){ callback(); } } }else{ script.onload = function(){ callback(); } } script.src = url; document.head.appendChild(script); } </script> </body> </html>
text是未定义变量,怎么解决?传一个匿名函数(是一个函数引用),要在匿名函数体内执行test()
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>loadScript</title> </head> <body> <script> loadScript('tools.js', function(){ test(); // 在匿名函数体内执行test() }); function loadScript(url, callback){ var script = document.createElement('script'); script.type = "text/javascript"; if(script.readyState){ script.onreadystatechange = function(){ if(script.readyState == "complete" || script.readyState == "loaded"){ callback(); } } }else{ script.onload = function(){ callback(); } } script.src = url; document.head.appendChild(script); } </script> </body> </html>
还有一种办法,
1). 可以把参数callback,变成字符串形式的,
2). loadScript('demo.js', 'test()' ); 传一个'test()'字符串
3). 字符串是没法执行的,把字符串放到eval里面,eval把里面的字符串当做函数代码来执行
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>loadScript</title> </head> <body> <script> loadScript('tools.js', 'test()'); function loadScript(url, callback){ var script = document.createElement('script'); script.type = "text/javascript"; if(script.readyState){ script.onreadystatechange = function(){ if(script.readyState == "complete" || script.readyState == "loaded"){ eval(callback); // eval把里面的字符串当做函数代码来执行 } } }else{ script.onload = function(){ eval(callback); // eval把里面的字符串当做函数代码来执行 } } script.src = url; document.head.appendChild(script); } </script> </body> </html>
还有一种更好的解决办法,需要跟tool.js函数库相配合,写成json对象的形式
// tools.js文件 var tools = { test : function(){ console.log('a'); }, demo : function(){ console.log('a'); } }
执行
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>loadScript</title> </head> <body> <script> loadScript('tools.js', 'test'); // 传属性名'test'也执行 function loadScript(url, callback){ var script = document.createElement('script'); script.type = "text/javascript"; if(script.readyState){ script.onreadystatechange = function(){ if(script.readyState == "complete" || script.readyState == "loaded"){ tools[callback](); // 这里写成这样的形式 } } }else{ script.onload = function(){ tools[callback](); // 这里写成这样的形式 } } script.src = url; document.head.appendChild(script); } </script> </body> </html>