为了账号安全,请及时绑定邮箱和手机立即绑定

阮一峰的ES6---Module的加载实现

浏览器加载
传统方法:在html网页中,浏览器通过<script>标签加载js脚本。

<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
  // module code
</script>

<!-- 外部脚本 -->
<script type="application/javascript" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="path/to/myModule.js">
</script>

因为浏览器脚本默认语言是js脚本,所以type="application/javascript"可以省略
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。

<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="path/to/myMethod.js" defer></script>
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="path/to/myMethod.js" async></script>

<script>标签打开defer或async属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer和async的区别:前者要等到整个页面正常渲染结束,才会执行,后者一旦下载完,渲染引擎就会终端渲染,执行这个脚本以后,再继续渲染,一句话,defer是“渲染完再执行”,async是下载完就执行,另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

加载规则
浏览器加载ES6模板,也是使用<script>标签,但是要加入type = “module”属性。

<script type= 'module' src = "foo.js"> </script>

网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。
浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

<script type="module" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="foo.js"></script>
<!-- 等同于 -->
<script type="module" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="foo.js" defer></script>

<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。

<script type="module" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="foo.js" async></script>

ES6模块允许内嵌在网页中,语法行为与加载外部脚本一致;

<script type="module">
  import utils from "./utils.js";

  // other code
</script>

外部的模块脚本(foo.js),有几点需要注意:
(1) 代码是在模块作用域之中运行,而不是在全局作用域运行,模块内部的顶层变量,外部不可见。
(2) 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,也可以使用export命令输出对外接口)
(3)模块之中,顶层的this关键字返回undefined,而不是指向window,也就是说:在模块顶层使用this关键字是无意义的;
(4)同一个模块如果加载多次只执行一次;

//实例模块
import utils from "https://www.baidu.com/js/utils.js";
const x =1;
console.log(x ===window)// false
console.log(this === undefined) //true
delete x ;//语法错误,不能删除x

利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。

const isNotModuleScript = this !== undefined;

ES6模块与CommonJS模块的差异
(1)CommonJS模块输出的是一个值的拷贝,ES6模块输出得失值的引用;
(2)CommonJS模块是运行时加载,ES6模块是编译时输出接口。
CommonJS模块输出的是值的拷贝,也就是说一旦输出一个值,模块内部的变化就影响不到这个值;

//lib.js
var counter = 3;
function inCounter (){
   counter++;
}
module.exports = {
 counter :counter,
inCounter : inCounter,
};

//在main.js里面加载这个模块
var mod = require(".lib");
console.log(mod.counter);//3
mod.inCounter();
console.log(mod.counter);//3

lib.js模块加载以后,他的内部变化就影响不到输出的mod.counter了,这是因为mod.counter是一个元素类型的值;
ES6模块的运行机制与CommonJS不一样,js引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

//lib.js
export let counter = 3;
export function inCounter () {
     counter++;
}

//main.js
import { counter,inCounter} from "./lib";
console.log(counter);//3
inCounter();
console.log(counter);//4

ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。

内部变量
ES6 模块应该是通用的,同一个模块不用修改,就可以用在浏览器环境和服务器环境。为了达到这个目标,Node 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。
首先,就是this关键字。ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异。
其次,以下这些顶层变量在 ES6 模块之中都是不存在的。
arguments
require
module
exports
filename
dirname
CommonJS模块的加载原理
CommonJS的一个模块,就是一个脚本文件,require命令第一次加载该脚本,就会执行这个脚本,然后在内存生成一个对象。

{
   id: "...",
    exports:{...},
     loaded:true,
    ...
}

Node内部加载模块后生成的一个对象,该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模板的脚本是否执行完毕,等等属性;
以后需要用到这个模块的时候就会到export属性上面取值,即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值,也就是说:CommonJS模块无论加载多少次,只在第一次加载时运行一次,以后再加载,就返回第一次运行的结果;

点击查看更多内容
8人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消