模块加载
js的标准规范有以下几种 CommandJS
,ES Module
,AMD
,CMD
,UMD
;
CommandJS
CommandJS
是服务端JS
的标准规范,特点是只能用于服务端。
核心语法为require
引入和module.exports
输出。
每一个文件是一个模块,有自己的作用域。在文件内定义的变量、函数、类都是私有的,对其他文件不可见。
每个模块内部,module变量代表当前模块,该变量是一个对象。他有一个exports属性,这个属性是对外的接口。加载某一个模块,其实就是加载该模块的module.exports
属性。
特点:
- 所有代码运行在模块作用域内,不会污染全局变量
- 模块可以加载多次,但是只有第一次加载时运行一次。然后运行结果就被缓存下来,以后再加载,就是直接读取缓存的结果。
使用(Node环境)
nodejs
里的规范,环境变量:module、exports、require、global
声明及引用
// 声明
module.exports.add = function(){}
// 引用
const math = require('./math')
math.add()
// 声明
module.exports = function(){}
// 引用
const math = require('./math')
math()
// 声明
module.exports = { huang: 'huang', jin: 'jin' }
// 引用
const { huang, jin } = require('./math')
module
node内部提供一个Module构建函数。每一个模块内部都有一个module对象,代表当前模块。它有以下属性
id
模块标识符,通常是带有绝对路径的模块文件名path
模块的文件名,绝对路径exports
模块对外输出的值,其他文件加载该模块其实就是读取module.exports
变量parent
调用该模块的模块children
该模块用到的其他模块loaded
该模块是否已经加载完(在父模块中require一个子模块之后,子模块的loaded才变为true)
循环依赖
Node的循环依赖处理方式是:如果有循环,执行到哪就算哪。简单来说就是,当A模块开始加载时,缓存中会立刻出现A模块的module.exports
,当require(b)
,并且b中require(a)
的时候,b只能获取到循环依赖之前的a。
//a.js
module.exports.max = 10
require(b)
module.exports.max = 20
//b.js
const a = require(a)
console.log(a.max) // 10
加载模式
加载模块的方式是同步的。在输入时先加载整个模块,生成一个对象。再从这个对象上读取方法,这种加载被称为运行时加载。只有对应子模块加载完成,才能执行后面的操作。 为什么是同步的?因为Nodejs
是用于服务端编程,模块文件存在于硬盘中,读取非常快。
加载时机
输入的值是被输出的值的拷贝。 父模块引入了一个子模块,其实引入的是这个子模块输出的值的拷贝,一旦输出了这个值,模块内部的变化就影响不到这个值了。
ES6 Module
异步执行,模块几乎同时导入,后面模块不需要等待前面模块导入完成。ES6 Module 输出的是值的动态引用,不会缓存。
ES6 在语言标准层面上,实现了模块功能,而且实现的非常简单,宗旨是在浏览器和服务器通用的模块解决方案。
ES6 Module中使用import
引入,export
输出。
特点:
import
是只读属性,不能赋值。相当于const
export/import
提升,import/export
必须位于模块的顶级,不可以位于作用域内,其次对于模块内的import/export
都会提升到模块的顶部。
使用
声明及引用
// 声明
export function add(){}
import { add } from './math'
export const name = 'box'
import { name } from './math'
// 引用math模块中所有方法并存放在module变量中。
export function add(){}
export const name = 'box'
import * as module from './math'
// 为模块指定默认输出,一个文件中只能有一个export default,且后面不能跟变量声明的语句
// 本质上,export default就是输出一个叫default的变量或者方法,然后系统允许你为它重命名。
export default { add }
import add from './math' // export default 声明 引用
function add(){}
export {add as default}; // 等同于export default add
import {default as foo} from './module'; // 等同于import foo from './module'
// export 与 import 的复合写法
export {foo,bar} from 'module';
// 等同于
import {foo,bar} from 'module;
export {foo,bar}
// 上面代码中,export和import语句可以结合为一行代码。但是,写成一行以后,foo和bar实际上没有被导入当前模块,只是相当于当前对外转发了这两个接口,导致当前模块不能直接使用foo,bar。
import()函数
import的模块需要静态分析,所以不能用于动态加载。也就不能完成required同样的功能,因此,引入了import()函数,返回一个Promise对象。
这个函数的引入起到的很好的作用,比如我们在做多语言加载的时候,我们需要引入语言包,但是我们又不想一次性将所有语言包全部引入,我们只需要引入需要的语言包就可以了,那么就用到了import()函数。
import (path).then(res=>{
console.log(res)
}).catch(err=>{
console.log(res)
})
环境加载
传统方法
script
标签默认是同步加载的,加上defer
和async
就会开启异步加载。
区别:
defer
要等到整个页面在内存中正常渲染结束,才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。另外,如果有多个defer
脚本,会按照他们在页面中出现的顺序加载,而多个async
脚本,是不能保证按顺序加载
es6
模块加载
浏览器加载es6
模块,也是用 <script>
标签,不过要加入 type="module"
属性。添加该属性后,默认开启 defer
属性。若想开启 async
属性,可以直接添加。作用同上。
<script type="module">
import {add, redis} from './module';
</script>
node.js
加载
node
中原本存在的commonJS
与es6
的模块加载并不兼容。因此node中做了限制
.mjs
文件总是以es6
模块加载; .cjs
文件总是以commonJS
加载, .js
文件的加载取决于 package.json
中type字段,若 type="module"
则以es6
模块加载,默认commonJS
循环依赖
ES6 Module
其实并不关心有没有循环依赖,他并不需要产生结果,他只需要给你一个引用即可,至于是否能取到值,那么就需要开发者自己来保证了。
加载时机
import
是静态命令的方式,js
引擎对脚本静态分析时,遇到模块加载命令import
,会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被记载的那么模块中去取值。模块内部引用的变化会反应在外部。
在import
时可以指定加载某个输出值,而不是加载整个模块,这种加载称为编译时加载。在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。
AMD
AMD
是RequireJS
使用的规范,是为了浏览器中的模块加载而实现的。浏览器环境要加载资源,需要从服务器端加载模块,依靠网络下载,时间比较长。所以需要采用非同步的模块。AMD
是异步加载。而AMD
的设计思路,也是参考了一部分CommandJS
的。
AMD
相关的api
define
,用于定义模块,如果我们定义的模块本身也依赖其他模块,那么就需要把它放在数组中,作为第一个参数
使用
// 声明
define(['jquery', 'underscore'], function ($, _) {
// methods
function a(){}; // private because it's not returned (see below)
function b(){}; // public because it's returned
function c(){}; // public because it's returned
// exposed public methods
return {
b: b,
c: c
}
});
// math.js 只有在有依赖的情况下,才需要进行定义依赖,否则可以直接传入内容,比如:
define(function(){
return {
add:function(){}
}
})
require(['math'], function(math) {
console.log(math.add());
})
循环依赖
强制忽略,比如有两个模块A,B。当A依赖B,然后B依赖A的时候,B获取到的A是为未定义的状态。而且总是会把依赖的模块执行完成,也就是说B一定会被先执行完成。
CMD
CMD
sea.js
它的各个方面和AMD
都非常像,只不过CMD
推崇依赖就近 延迟加载,AMD
推崇的是前置依赖
define(function(require,exports,module){
const a = require('a')
a.x()
})
但是也是延迟执行,还是要等加载完。判断方式就是正则啦。
Function.prototype.toString.call(function(){
console.log('i am Magic')
})
UMD
UMD
是一个为了解决跨平台MD导入问题(是一种思想),它的解决方法就是通过揉合CommandJS
和AMD
CMD
来解决这个问题。
先判断是否支持Nodejs
模块(exports
是否存在),如果存在就使用Nodejs
模块。不支持的话,再判断是否支持AMD
/CMD
(判断define
是否存在)。都不行就挂载在window
全局对象上
// if the module has no dependencies, the above pattern can be simplified to
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
root.returnExports = factory();
}
}(this, function () {
// Just return a value to define the module export.
// This example returns an object, but the module
// can return a function as the exported value.
return {};
}));
其实它就是自定义了一种包装方式,并且对每种环境下进行不同处理。如果是有依赖的情况,一样会进行特殊情况处理。
对比
* | AMD | CommandJS | UMD | CMD | ES Module |
---|---|---|---|---|---|
使用在浏览器 | ✅ | ❌ | ✅ | ✅ | ✅ |
使用在服务端 | ❌ | ✅ | ✅ | ❌ | ✅ |
异步加载 | ✅ | ❌ | ✅ | ✅(允许) | ✅(允许) |