JavaScript深入之模块化

354 阅读6分钟

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具有缓存,第一次加载的时候,会完整运行整个模块并输出到一个对象中,下次加载该文件时,会直接从内存中读取该对象 image.png
由上图可以看出,info对象 和module.exports和why指向的是同一个对象,所以当有一个去改变值时,这个值就被改变了

module.exports和exports的区别和联系?
exports = module.exports = {};

注意:但是node中真正用于导出的是module.exports对象,而不是exports

但是exports为什么也可以导出呢?

因为module对象的exports属性是exports对象的一个引用

image.png
exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出 当同时使用module.exports和exports时,只会输出modules.exports的内容

require的查找规则

情况一:如果是node的核心模块,那么直接返回这个核心模块,并且立即停止查找

image.png

模块的加载过程

  • 模块在第一次加载的时候,模块里的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(连接)”,把模块指向对应的内存地址
  • 阶段三:运行,运行代码,计算值,并且将值填充到内存地址中

阶段一:构建

  1. 查找:首先找到入口文件,在HTML中,使用script标签去找到入口文件,然后通过import去寻找与之关联的其他按模块,形成依赖关系树(AST)
  2. 下载:遍历AST树,下载这些依赖项
  3. 将文件解析为模块记录
    如下图:我们可以看出一个模块记录包含了当前模块的AST,引用了哪些模块的变量,已经一些特定的属性和方法

image.png

注意:当去下载这个模块的时候,会把这个URL放入module map中,一旦Module Record被创建时,它就会被记录在模块映射Module Map中 当有其他模块依赖这个文件时,就不会去重复下载了,而是直接采用 Module Map中url对应的Module Record image.png
在构造过程结束,从主入口文件变成了一堆模块记录

阶段二:实例化
实例化:将构造的模块实例化并将所有实例链接在一起
JS引擎会建立模块环境记录(dodule environment record),他管理着对应module rcord中的变量,在内存中找到该变量所对应的内存位置,此时内存中这些位置还不会有值(为undefined),只有在计算后才有值
pass:导出和导入都指向内存中的同一位置,先链接导出保证了全部的导入都能找到对应的导出。
ES Module的这种链接方式被称为 Live Bingdings(动态绑定)

image.png 阶段三:求值

执行代码,填充内存中的值
求值阶段确定了Module文件中变量的值,由于 ES Module使用的是动态绑定(指向内存地址),export中修改数据会映射到内存中,import数据相应也会改变

image.png