「这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战」
CommonJs规范和node实现
exports :导出对象
module.exports:导出对象
require( ):导入对象
真正用于导出对象的是module.exports 而不是exports, node内部默认实现的是module.exports=exports(exports本身是一个对象)
CommonJs的加载是同步的:同步意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;
模块加载过程
1、模块在被第一次引入时,模块中的js代码会被运行一次
2、模块被多次引入时,会缓存,最终只加载(运行)一次
3、如果有循环引入,node采用深度优先算法运行引入的模块。
require细节
require导入的三种常见规则:
-
导入的X为node核心模块,如path,http时,直接返回核心模块停止查找。
-
导入的X是以 ./ 或 ../ 或 /(根目录)开头的 ,
一:将其看做一个文件来查找,如果有后缀名,直接找到相应的文件。
如果没有后缀名,按照一下顺序进行查找:
- 查找x.js文件
- 查找x.json文件
- 查找x.node文件
二:如果没有找到对应的文件,将X当做一个文件夹,在其下面查找以下文件:
- X/index.js
- X/index.json
- X/index.node
如果还是没有找到,返回not found
-
直接导入一个X,X也不是核心模块,会在当前的文件夹的node_module中查找,如果没有找到,然后向上层目录的node_module中查找。都没有找到,则返回not found。
commonJS的加载过程
commonJS是运行时加载的,并且是同步加载。
- 运行时加载表示js引擎在执行js代码时加载模块。
- 同步意味着在加载模块时,后续代码是无法运行的。
ES module
es6之后js中有了ES module 模块化 。ES module通过import 和export关键字来导入和导出。ES moudule是异步加载。
使用了ES module 会自动采用严格模式。
使用方式
浏览器下:
在浏览器下,要通过建立服务来打开页面,直接通过本地文件形式打开会包CORS错误。
//添加type='module',才会做为module
<script src="./异常.js" type="module"></script>
node环境下:
在node环境下,node环境下,如果没有package文件,我们需要将js文件的后缀名写为mjs,node才会将其作为模块来使用。
import导入方式
-
:import {标识符列表} from '模块'
import {name,age} from './foo.js' -
导入时标识符起别名
import {name as fname,age as fage} from './foo.js' -
通过 * 将模块功能放到一个模块功能对象(a module object)上
import * as baz from './foo.js'
export导出方式
-
:在语句声明的前面直接加上export关键字
export const name='wang' -
:将所有需要导出的标识符,放到export后面的 {}中
注意::这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的
export { name, age, foo } -
导出时给标识符起一个别名
//将name,age,foo名字改为Fname,Fage,Ffoo export { name as Fname , age as Fage, foo as Ffoo }
export和impot的结合使用
//直接从aaa.js中导入d,c后直接导出
export { d, c } from './aaa.js';
应用场景:当我们在封装自己的时,每个文件都有导出的接口,这样使用者必须知道哪一个文件导出了哪一个接口,对使用者很方便。通过在一个文件中导入所有的接口,然后直接导出,这样使用者只需要知道这一个接口文件就可以使用这个库了。
default
//foo.js
export default foo;
//index.js
//不需要知道导出的变量名,可以自定义变量名
import baz from './foo.js'
import加载
import加载是在代码解析时就要确定模块之间的导入和导出关系,所以不能在逻辑代码中使用。
es给我们提供了一个import()函数,可以动态的加载模块。
let tag = true;
if (tag) {
import ('./foo.js').then((res) => {
console.log(res.foo.name);
})
}
并且ES module 加载是异步的,加载过程不会阻塞其他文件的加载。
ESmodule在浏览器中的解析过程
解析分为三个阶段:
- 构建(Construction)根据地址查找js文件,并且下载,将其解析成模块记录(Module Record)
- 实例化(Instantiation),对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址。
- 运行(Evaluation),运行代码,计算值,并且将值填充到内存地址中。
阶段一:构建阶段
构建:下载main.js模块文件,浏览器会获取到main.js文件,然后浏览器对main.js文件进行静态解析(不会运行代码),生成一个Module Record 数据结构,Module Record中的RequestedModules记录依赖的文件。然后下载main.js依赖的js文件,生成相应的module Record数据结构,如果counter.js和display.js依赖其他文件,继续加载相应的依赖文件,并进行静态解析。直到解析到没有依赖更多文件后终止。(浏览器中有一个Map,会记录已下载和正在下载的文件,保证了不会重复下载同一个文件)
阶段二和阶段三:实例化阶段-求值阶段
实例化阶段:会将Module Record(模块记录)进行实例化为一个Module Environment Record(模块环境记录) ,在内存中保存导出的值,然后对模块记录导出的值进行绑定,main.js是对导入的值进行绑定。此时内存中变量的值都是undefined。
求值阶段:运行下载的文件,计算出内存中变量的值,替换内存中保存的变量的值。
注意:导出的模块可以改变变量的值,导入的模块不可以改变变量的值。
ES module加载过程
export在导出一个变量时,js引擎会解析这个语法,并且创建模块环境记录,模块环境记录会和变量进行 绑定(binding),并且这个绑定是实时的,而在导入的地方,我们是可以实时的获取到绑定的最新值的。
//foo.js
var name = 'wang';
var age = 23;
var foo = {
name: 'fawef'
}
//大括号不是对象
setTimeout(() => {
name = 'list';
}, 1000);
export { name, age };
//index.js
import { name, age } from './foo.js'
console.log(name); //wang
console.log(age); //23
setTimeout(() => {
console.log(name);//list
}, 2000);
AMD
AMD是异步加载的规范,使用在浏览器端。使用requirejs库来实现加载。
导入:
require.config({
baseUrl: "src",
paths: {
//定义模块
a: "./a",
b: "./b",
},
});
//加载模块
require(["a"], function (foo) {
console.log(foo);
});
导出:
define(function () {
const name = "why";
const age = 18;
function sum(num1, num2) {
return num1 + num2;
}
return {
name,
age,
sum,
};
});
CMD
使用seajs进行异步加载。
回调函数传入三个参数:
- require() 函数导入文件
- exports导出变量和对象
- moudle.exports导出变量和对象
define(function (require, exports, moudle) {
const re = require("./foo.js");
console.log(re);
});
commonJs和ES module的交互
-
在浏览器中commonJs和ES module不可以相互导入和导出。
-
在node中:
-
commonjs是不可以加载esmodule
原因:因为CommonJS是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码;
-
esmodule可以加载commonjs
原因:ES Module在加载CommonJS时,会将其module.exports导出的内容作为default导出方式来使用
-
-
在平常开发中,基于webpack的环境下commonJS和ES module可以相互加载。