ES6的学习笔记(十五)Module的加载实现。。。

345 阅读4分钟

Module的加载实现

1.浏览器加载

传统方法

在HTML网页中,浏览器通过<script>标签加载JavaScript脚本。

<script type="application/javascript">
  // 这个是页面内部的脚步
</script>

<script type="application/javascript" src="path/to/myModule.js">
	// 这个是页面外部的脚本
</script>

// 浏览器默认的脚本语言是JavaScript,所以 type="application/javascript"可以省略。
// 默认,浏览器同步加载JS脚本,所以引擎遇到<script>标签,就停下来,等到脚本执行完毕后,再向下渲染。如果是外部脚本,必须加入脚本下载的时间。

若脚本体积很大,下载和执行的时间会很长,造成浏览器堵塞,用户体验不好,因此,脚本可以异步加载。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
// 打开defer或者async属性,脚本就会异步加载。
// defer:等到整个页面在内存中正常渲染结束(DOM结构完全生成,以及其他脚本执行完成),才会执行;
// async:一旦下载完成,渲染引擎就会中断渲染,执行这个脚本后,再继续渲染。
// defer渲染完再执行,async下载完就执行。有多个defer脚本时,会按照在页面中出现的顺序加载,多个async脚本不能保证加载顺序。

加载规则

浏览器加载ES6模块时要加入type="module"属性。

<script type="module" src="./foo.js"> </script>
// 对于带有type="module"的脚本,浏览器都是异步加载,即等到整个页面渲染完成后,再执行模块脚本,等同于defer属性。
// 等同于
<script type="module" src="./foo.js" defer></script>
// 网页中的多个<script type="module">,会按照在页面中出现的顺序依次执行。

async属性也可以打开,但是打开async属性之后,<script type="module">就不会再按照在页面中出现的顺序执行了,而是只要该模块加载完成,就执行该模块,渲染引擎就中断渲染,直到模块执行完成之后,再恢复渲染。

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

ES6模块可以内嵌在网页中,语法同加载外部脚本。

<script type="module">
	import utils from './utils.js'
    // code
</script>
// jQuery就持模块加载
<script type="module">
	import $ from './jquery/src/jquery.js'
    $("#message").text('学习ES6,学好一点!')
</script>

对于外部的模块脚本,例如上述的foo.js。

  • 代码不是在全局作用域中运行,而是在模块作用域中运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管是否声明use strict。
  • 模块之中,可以使用import命令加载其他模块,(.js后缀不可省略,需要提供绝对URL和相对URL),可以使用export命令输出对外接口。
  • 模块中,顶层的this关键字返回undefined,不是指向window。
  • 多次加载同一个模块,只执行一次。

2.ES6模块与CommonJS模块的差异

ES6模块与CommonJS模块完全不同,有三个重大差异。

  • CommonJS输出值的拷贝,ES6模块输出值的引用。
  • CommonJS模块是运行时加载,ES6模块是编译时输出接口。
  • CommonJS模块的require()是同步加载模块,ES6模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。 其中,第二个差异是因为CommonJS加载的是一个对象(即module.exports属性),此对象在脚本运行完后才会生成。ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。 解释第一个差异:
    CommonJS模块输出值的拷贝,即一旦输出一个值,模块内部的变化就影响不到这个值。
// lib.js
let counter = 3;
function incCounter(){
	counter++; 
}
module.exports = {
	counter:counter,
    incCounter:incCounter
}
// main.js  在main.js中加载这个模块。
var mod = require('./lib');
console.log(mod.counter) //3
mod.incCounter();
console.log(mod.counter) // 3 输出的值没有任何变化。
// 说明,lib.js模块加载之后,它的内部变化就影响不到输出的mod.counter。
// 因为mod.counter是一个原始类型的值,会被缓存。写成一个函数,才能得到内部变动后的值。
let counter = 3;
function incCounter(){
	counter++;
}
module.exports = {
	get counter(){
    	return counter;
    },
    incCounter: incCounter
}
// 上述代码中,输出的counter属性实际上是一个取值器函数。再执行main.js,就能正确的读取内部变量counter的变动了。

ES6模块的运行机制。
当JS引擎对脚本静态分析的时候,遇到import命令就生成一个只读引用。等脚本真正执行时,再到被加载的那个模块去取值。
即,ES6模块是动态引用,不会缓存值,模块里面的变量绑定其所在的模块。

// lib.js
export let counter =3;
export function incCounter(){
	counter++;
}
// main.js
import {counter,incCounter} from './lib.js'
console.log(counter) // 3
incCounter()
console.log(counter) // 4
// 即输入的变量counter是活的,反应其所在模块lib.js内部的变化。

另一个例子

// m1.js
export var foo ='bar'
setTimeout(() => {
    foo = 'baz'
}, 500);
// m2.js
import {foo} from './circle'
console.log(foo)
setTimeout(() => {
    console.log(foo)
},500)
// m2.js 会正确读取foo的变化,证明ES6模块是动态的去被加载的模块取值,并且变量总是绑定其所在的模块。

ES6输入的模块变量,只是符号连接,这个变量仅为可读,对其赋值会产生错误。

// lib.js
export let obj = {}
// main.js
import { obj } from './circle.js'
obj.pro = 'hello'
obj = {} // TypeError:Assignment to constant variable
// 变量obj指向的地址仅为可读,不能重新赋值,即相当于在main.js中创造了一个const变量。

export通过接口输出的是同一个值,不同的脚本加载这个接口,得到的是同一个实例。

// f.js
function C() {
    this.num = 0
    this.add = function () {
        this.num += 1
    }
    this.show = function () {
        console.log(this.num)
    }
}
export let c = new C()

// f1.js
import {c} from './profile.js'
c.add()

// f2.js
import {c} from './profile.js'
c.show()

// main.js
import './f1.js'
import './f2.js'
// 最终在控制台输出的值是1,证明f1模块和f2模块中引入的的是同一个实例。