Go to comments

JavaScript 异步加载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异步加载有三种方法


二、第一种方法 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会把所有下载多的东西展示到这里面去,所有的网络请求都放在这

image.png

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


为什么这个肯定呢?

因为有个灯塔模式灯塔模式是创建一个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>

不能执行

image.png


为什么不能执行呢?

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

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

就可以执行了

image.png


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

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

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>

image.png


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>



Leave a comment 0 Comments.

Leave a Reply

换一张