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模块中引入的的是同一个实例。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!