js模块化出现的前因:
以前js的出现是为了做一些简单的交互,但是随着前端的发展,js能做的东西就越来越多,为了维护更加的方便,逐渐开始出现模块化
模块化就是实现特定功能的文件,有了模块,我们可以更方便的使用代码,比如我需要什么模块的功能,我就加载什么模块,但是模块化开发需要遵守一些规范,否则就乱套了,后来就慢慢大家所熟悉的有了 AMD CDM CommonJS。
什么是模块化?什么是模块化开发
- 模块化开发可以理解为把程序分为一个个小的模块,
- 每个模块构有自己的逻辑代码,自己的作用域,不会影响到其他模块,
- 每个模块希望可以暴露自己的变量,函数等,也可以通过某种方式导入其他模块的变量、函数等。
- 按照这种模块划分开发的过程,叫做模块化开发
模块化开发需要遵守一些规范,所以就有了AMD CDM CommonJs规范,此篇重点讲下CommonJs 和Esmodule 规范
CommonJs规范
commonjs是模块化的一种规范
概述:node.js由模块组成,每个文件就是一个模块,有自己的作用域。这个模块包括commonjs规范的核心变量:
exports、 module.exports、 require
exprots、module.exports:对模块的内容进行导出
require:导入其他模块的内容
commonjs的加载机制:
commonJs具有缓存,第一次加载的时候,会完整运行整个模块并输出到一个对象中,下次加载该文件时,会直接从内存中读取该对象
由上图可以看出,info对象
和module.exports和why指向的是同一个对象,所以当有一个去改变值时,这个值就被改变了
module.exports和exports的区别和联系?
exports = module.exports = {};
注意:但是node中真正用于导出的是module.exports对象,而不是exports
但是exports为什么也可以导出呢?
因为module对象的exports属性是exports对象的一个引用
exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出
当同时使用module.exports和exports时,只会输出modules.exports的内容
require的查找规则
情况一:如果是node的核心模块,那么直接返回这个核心模块,并且立即停止查找
模块的加载过程
- 模块在第一次加载的时候,模块里的js代码就会被执行一次
- 同一模块被多次引入,最终只会加载一次,因为会被缓存
- 如果有循环加载呢?: 模块的加载顺序采用深度优先算法
commonjs的原理其实就是对象的赋值
common.js规范的缺点
commonjs加载模块是同步的,要等引入的模块加载完成之后,才会执行当前模块的内容,所以不能用于浏览器
ESmodule模块化
ES Module和CommonJS的模块化有一些不同之处:
- 一方面它使用了export和import关键字
- 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式 pass:采用ESModule将自动采用严格模式:use strict
export关键字用法
//1.第一种方式: export 声明语句
export const name = "foo"
export const age = 18
export function foo() {
console.log("foo function")
}
export class Person {
}
// 2.第二种方式: export 导出 和 声明分开
const name = "why"
const age = 18
function foo() {
console.log("foo function")
}
export {
name,
age,
foo
}
// 3.第三种方式: 第二种导出时起别名
export {
name as fName,
age as fAge,
foo as fFoo
}
import关键字
//1.导入某个模块的变量
import { name, age } from "./foo.js"
//2.导入时给标识符起别名
import { name as n, age as a} from "./foo.js"
//通过*将模块功能放到一个模块功能对象上
// 3.导入方式三: 将导出的所有内容放到一个标识符中
import * as foo from './foo.js'
export和import结合使用
//导入math模块和format模块的所有内容,并且将它放到一个标识符中导出去
export * from './math.js'
export * from './format.js'
默认导出export default
//默认导出,在导入时不需要使用{},并且可以自己来指定名字
const foo = "foo value"
export default foo
每个模块中只能有一个默认导出,在导入时不需要使用{},并且可以自己来指定名字
//按上图导入 我们可以字段指定导入的名字,上面导出的是foo,导入的时候我们取的bar名称
import bar from './foo.js'
ESModule的解析流程
ESModule的解析过程可以划分为三个阶段:
- 阶段一:构建,根据地址查找js文件,并且下载,将其解析成模块记录(Module Record,一种数据结构)
- 阶段二:实例化,对模块记录进行实例化,并且分配内存空间,解析模块的导入导出语句,让导出和导入都指向内存里面的这些盒子。这叫作“linking(连接)”,把模块指向对应的内存地址
- 阶段三:运行,运行代码,计算值,并且将值填充到内存地址中
阶段一:构建
- 查找:首先找到入口文件,在HTML中,使用script标签去找到入口文件,然后通过import去寻找与之关联的其他按模块,形成依赖关系树(AST)
- 下载:遍历AST树,下载这些依赖项
- 将文件解析为模块记录
如下图:我们可以看出一个模块记录包含了当前模块的AST,引用了哪些模块的变量,已经一些特定的属性和方法
注意:当去下载这个模块的时候,会把这个URL放入module map中,一旦Module Record被创建时,它就会被记录在模块映射Module Map中 当有其他模块依赖这个文件时,就不会去重复下载了,而是直接采用 Module Map中url对应的Module Record
在构造过程结束,从主入口文件变成了一堆模块记录
阶段二:实例化
实例化:将构造的模块实例化并将所有实例链接在一起
JS引擎会建立模块环境记录(dodule environment record),他管理着对应module rcord中的变量,在内存中找到该变量所对应的内存位置,此时内存中这些位置还不会有值(为undefined),只有在计算后才有值
pass:导出和导入都指向内存中的同一位置,先链接导出保证了全部的导入都能找到对应的导出。
ES Module的这种链接方式被称为 Live Bingdings(动态绑定)
阶段三:求值
执行代码,填充内存中的值
求值阶段确定了Module文件中变量的值,由于 ES Module使用的是动态绑定(指向内存地址),export中修改数据会映射到内存中,import数据相应也会改变