Go to comments

JavaScript 异步加载JS

异步加载JS

js加载的缺点:加载工具方法没必要阻塞文档,过多js加载会影响页面效率,一旦网速不好,那么整个网站将等待js加载而不进行后续渲染等工作。

有些工具方法需要按需加载,用到再加载,不用不加载。


一、为什么要异步加载JS

1、JS文件是怎么加载的?

js 文件是同步加载的,

加载到 js 文件的时候就卡在那了,阻断了 html 和 css 的加载线,等 js 文件加载完并且执行完之后,html 和 css 在继续下载


比如,引入 tools.js 文件

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script type="text/javascript" src="./tools.js"></script>
</head>
<body>

<h1>文章标题</h1>

</body>
</html>


js 文件内容是

alert( '阻断HTML和CSS的加载线,看不到h1标签' )


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,能不能办到呢?

后续由于技术的更迭,可以办到了,

这是异步加载 js 的一个需求


二、异步加载 js

javascript 异步加载有三种方法

1. defer 异步加载,但要等到 dom 文档全部解析完才会被执行。只有 IE 能用。

2. async 异步加载,加载完就执行,async 只能加载外部脚本,不能把 js 写在 script 标签里。

    注,1.defaer 和 2.async 执行时也不阻塞页面

3. 创建 script,插入到 dom 中,加载完毕后 callBack


1、defer

想让 js 变成异步加载的方式很简单,

在 script 的头标签上加 defer,从此之后这个 js 就变成异步加载的 js 了

<!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 不会阻断 html 和 css 的下载,

js 会和 html、css 并行的下载,各自下载各自的,互相不会影响 

defer 有一个小问题,只有 IE9 以下可以用


凡是这种  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>


异步加载的 defer 什么时候执行?

defer 的执行时刻是要等到,整个文档全部解析完才会被执行


正常的 js 标签下载完立即执行,js 执行完后才会加载 html 和 css,

而 defer 这种 js 标签,不是下载完立即执行,而是要等到整个页面全部解析完才会执行。


什么是整个全部页面解析完呢?

也就是说 dom 树生成完,就是整个浏览器把标签从第一行扫到最后一行,把 dom 树构建完了,叫整个页面的解析完毕。


解析完毕”一定发生在“页面加载完毕”之前,

因为可能还有一些图片文件或文字还没下载完,所以 defer 标签的 js 执行时刻发生在整个“页面解析完毕”时。


Ps:

解析完,dom 树加载完

加载完,图片、文字、音频、视频...都下载完


2、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 和 defer 不一样的地方

1. async 是 W3C 的标准方法

2. defer 要等到整个文档全部解析完毕才执行,

    async 是加载完立马就执行并且是异步的,执行也是异步的,它不会影响页面里的其它东西

2. 还有一条是 async 只能加载外部脚本,也就是说不能在 script 里面写代码,只能在 script 里面写 src 等于一个外部脚本


async 的简单记忆法

asychronous 是异步的意思,把这个单词记住了 async 就好记了,async 是异步这个词的一个缩写

asychronous 再加几个单词 javascript、and、xml 就是 ajax 的缩写


3、defer 和 async 的总结

现在实现异步脚本的加载有两种方法

defer 方法是 IE 用的,

async 方法是标准浏览器用的,当然 IE 高版本浏览器也是可以用的


defer 要等整个文档解析完毕,才会执行它加载完的脚本

async 加载完脚本就立马执行


这两个在执行脚本的时候都是异步的,都不用影响页面其它部分的加载(不会去阻塞的)


有一个区别

defer 里面除了可以引入外部的 js 文件,让外部的 js 文件变成异步的以外,还可以让内部的 js 文本变成异步的,也就是说可以把代码写在 js 标签里面

async 只能加载外部的 js 文件,这是一个典型的区别


async 与 defer 的选择

https://zhuanlan.zhihu.com/p/637269351


4、defer 和 async 兼容的问题

现在有一个问题,

兼容性不好搞定,想让任何浏览器都能实现异步脚本的加载怎么办?


 <script type="text/javascript" src="tools.js" defer="defer" async="async"></script> 

这两一起写就崩溃了,问题这两个一起写也不行啊,在 IE9 以上的浏览器又能识别 defer 又能识别 async ,它两就冲突了吗!


换另一种方法,

一个 script 标签写 async

一个 scrIpt 标签写 defer 加载同样的脚本

<script type="text/javascript" src="tools.js" async="async"></script>

<script type="text/javascript" src="tools.js" defer="defer"></script>

理论上没毛病,但从代码上加载两次同样的脚本,冲不冲突先不说,可能有问题,

因为 async 上面的脚本加载完了之后会异步执行

下面 defer 的脚本会加载完后会等到解析完才会执行


他两谁先执行不一定,

而且这两脚本加载完后,代码会重叠、会发生覆盖,

有可能第一个脚本在执行代码的时候把值赋完了,第二个脚本拿到原来的值再处理就发生错误了,


因为它两执行的时刻都不一样,代码覆盖代码重叠产生执行顺序的冲突,这样也是不行的,

那怎么办呢?就引出了第三种方法


三、创建 script,插入到DOM中,加载完毕后 callBack

第三种方法就比较高端了,通杀所有浏览器,而且也是最常用的方法

1. 非常强大除了可以实现异步加载以外

2. 还可以按需加载,我什么候需要这个脚本,什么时候加载过来


1、什么是按需加载呢?

比如说页面上有一个按钮,

这个按键点击完之后,会生成一大堆新的东西,

但是经过统计,用户只有 0.01% 的概率会按这个按钮,


那这个按钮点击完之后,需要的方法,需要执行的东西可以放到一个 js 文件里面,

当用户去点击的时候,动态的下载完这个 js 文件,然后再去执行。


因为有太大的概率不需要这些代码,没必要让它加载到页面占内存空间,

所以有些时候是按需加载,需要的时候再加载过来,会有这样的情况,

总之一切的情况都是为了优化效率。


2、异步加载 js

这才是今天的重点

1. 在页面里可以动态的生成一个 dom 结构,还可以创建一个 script 标签

2. 创建完后,给 script 标签一个 type 属性与值(当然写不写是无所谓的,type 值是无所谓的)

3. 接下来给 script 标签加 src 属性   script.scr = "tools.js" 

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>异步加载js的第三种方法</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"

  </script>

</body>
</html>


注意

写到第 3 步让 src 等于值 "tools.js" 的时候,

 script.src = "tools.js"   这句执行完,系统就会下载 tools.js 地址里面的东西了,

就是开启一个线程加载 srcipt 标签了,而且下载的过程中也是异步的去下载,

但是下载了完了,什么时候执行呢?

如果代码只写了这么 3 步,它永远不会执行,它只会去下载,那什么时候才会执行呢?


4. 当把创建的 script 标签插入到 document.body 页面(或者插入到 document.head )里面去的时候才会执行

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>异步加载js的第三种方法</title>
</head>
<body>

  <script>

    var script = document.createElement('script');
    script.type = "text/javascript";
    script.src = "tools.js";
    // 4.当把script标签插入到页面里面去的时候,才会解析这个脚本,否则只是下载完什么都不干
    document.head.appendChild(script); 

  </script>

</body>
</html>


3、灯塔模式

tools.js 文件,文件内容是

alert('异步加载');


把最后一行注释掉,保存刷新会执行吗?

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>异步加载js的第三种方法</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,所有下载多的东西展示到这里面去,所有的网络请求都放在这

image.png

有点遗憾没有显示,但确实是下载了,理解一下有,肯定下载了。


为什么这个肯定呢?

因为有个灯塔模式,灯塔模式是创建一个 img 标签,

然后让 img 标签只作为一个预加载的层面,不去加载到页面里面去,

让 img.src = xxx 里面的值赋过来之后,形成一个预加载,以后用的时候方便,

不用二次加载,去拿缓存的就可以了,这是一个预加载机制。


这块 script.src 也会把 tools.js 下载过来的,

当 document.head.appendChild(script) 把 script 签添加到页面里面去的时候它才会去执行

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>异步加载js的第三种方法</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>异步加载js的第三种方法</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>

不能执行

image.png


为什么不能执行呢?

也不是真的不能执行,加一个定时器,在一秒之后再执行,就可以执行了

<script>

  var script = document.createElement('script');
  script.type = "text/javascript";
  script.src = "tools.js";
  document.head.appendChild(script);

  setTimeout(function(){
    test();
  }, 1000);

</script>

可以执行了

image.png


为什么一秒之后能执行,当前执行不了呢?

因为还没下载完,程序执行是以微秒计时的,

1. 下载 script.src = "tootls.js" 需要发一个请求,等请求响应完后,回归这个资源,

    有一个发请求,回归资源的一个过程。

2. 在发生这个过程中,系统就把 script.src = "tools.js" 下面的 test() 执行完了,

    因为程序的执行是非常快的,

    并且这个 script.src = "tootls.js" 下载,还是异步下载的,没有阻塞在这行等着下载

3. 所以当程序执行  document.head.appendChild(script)  把 script 标签添加到页面里面去的时候,可能 tools.js 还没下载完呢,

    当程序执行 test() 的时候,tools.js 可能还没下载完呢


问题来了,

我引入的是工具方法,引入之后无疑是想让 test() 执行,

现在什么时候执行成问题了,什么时候下载完能用,不能等秒数!


能不能有一个提示我们的机制 tools.js 下载完了,得到提示下载完了之后再调用他的 test() 方法?


5、load 事件

有一个机制叫 load 是一个事件,

之前说过 window 上面有一个 load 事件(window.noload),

没说只有 window 上有,load 不只 window 有,但凡能下载的都有 load 事件


 script.onload  

onload 事件代表当触发 load 事件的时候就代表下载完了

<script>

  var script = document.createElement('script');
  script.type = "text/javascript";
  script.src = "tools.js";
  script.onload = function(){ // 它的兼容性非常好
    test();
  }
  document.head.appendChild(script);

</script>

当下载完了我们再调用 test() 就能执行,每次刷新都能打印出 'a'

这就确保了下完之后再去执行 test() 方法,

如果下载不完就永远不去执行,他的兼容性非常好标准浏览器都兼容,就 IE 不兼容,


IE 就 script 标签上没有 load 事件,

IE 有自己的语法,提供了一套完整方法。


6、IE 有一个状态码 readyState 

IE 非常特殊,它有一个状态码 readyState,

谁上面的状态码呢?是 script 标签上的状态码。


readyState 状态码就是一个属性,这个属性里面存值了,一开始值是 loading

readyState 会根据 script 标签加载的进度,去动态改变的值,

如果 script 标签加载完,readyStaue 的值会改成 complete


script.readyState   一开始是 loading

script.readyState   如果标签加载完了会改成 complete

script.readyState   当值变成 loading 的时候代表加载完了


然后监听 readyState,

1. IE 里面提供了一个事件 onreadystatechange

2. 当 readyState 发生改变的时候,会触发这个 onreadystatechange 事件

<script>

  var script = document.createElement('script');
  script.type = "text/javascript";
  script.src = "tools.js"; 

  // 这个事件监听状态码什么时候变,ajax时还会在接触这个事件
  script.onreadystatechange = function(){ // 状态码改变一次,函数里面就会触发一次
    // script.readyState等于conplete或者loaded代表加载成功了
    if(script.readyState == "complete" || script.readyState == "loaded"){
      test();
    }    
  }

  document.head.appendChild(script);

</script>


把 IE 的和非 IE 的两个方法和在一起

<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>


五、封装一个兼容函数 loadScript

封装成一个 loadScript 函数,当需要异步加载一个 script 标签的时候用这函数


函数里面传两个参数

1. 第一个参数,

每次加载的 js 文件都不一样,把 src 的值变成参数 url 等待用户传


2. 还有一个参数是什么?

按需加载 script 标签,是为了执行一个函数,每次执行的函数都是未知,怎么把这个函数传进去?


我们把绑定的事件处理函数叫回调函数,为什么叫回调函数?

当满足一定条件才执行的函数叫回调函数,

这块也是当满足一定条件才执行它也叫回调函数,回调函数有一个名字叫 callback,

所以这里的第二个参数叫 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 瞬间到终止状态了,事件 onreadystatechange 永远不会触发了


事件 onreadystatechange 的 触发依赖于从 loading 变成 complete 这样一个过程,在绑定事件之前就完事了,绑定还有什么用?


解决方法是把 script.src = url 这句放到绑定 onreadystatechange 事件之后,

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('tools.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>

image.png

loadScript('tools.js', text) 执行的时候,

还不知道 text 是什么,所以报一个错误,text 是未定义变量,怎么解决?

传一个匿名函数(function(){} 叫一个函数引用),要在匿名函数体内执行 test(),callback 是匿名函数的引用

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);
}


还有一种办法,

1. 可以把参数 callback,变成字符串形式的,

2. loadScript('tools.js', 'test()' ) 传一个 'test()' 字符串

3. 字符串是没法执行的,把字符串放到 eval 里面,eval 把里面的字符串当做函数代码来执行

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"){
        eval(callback); // eval把里面的字符串当做函数代码来执行
      }
    }
  }else{
    script.onload = function(){
      eval(callback); // eval把里面的字符串当做函数代码来执行
    }
  }
  script.src = url; 
  document.head.appendChild(script);
}


还有一种更好的解决办法,

需要跟 tool.js 函数库相配合,函数库写成 json 对象的形式

var tools = {
  test: function(){
    console.log('a');
  },
  demo: function(){
    console.log('a');
  }
}


里面这样

1. tools[ ]  

2. tools[ callback ]    callback 就是传的 'test'

3. tools[ callback ]()  执行

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);
}



Leave a comment 0 Comments.

Leave a Reply

换一张