esmodule揭秘

139 阅读5分钟

最近在梳理关于esmodule的知识点,做了一些整理

esmodule是什么

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 **CommonJS 和 AMD **两种。前者用于服务器,后者用于浏览器。

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性

esmodule 模块定义

esm定义模块主要分为以下3个部分

模块定义(导出)

必须使用module.exports或exports定义模块

export  let num = 1;

export const getNum = () => {
  return num;
}

export const setNum = () => {
  num = num + 1; 
}


export default num;

模块使用(导入)

必须使用import关键词来导入模块,导入模块的时候可以起别名

// 导入export暴露的参数
import  { getNum, setNum, num } from './export.default.js';

// 给导入export暴露的参数起别名,在使用的时候使用别名
import { name as name1 } from './export.js';

// 导入export default暴露的参数
import num from './export.default.js';

// 将export暴露以及export default暴露的参数全部导入
import * as object from './export.default.js';

模块标识

本质上就是传给import... from, from后面的参数,他可以是具体的文件路径(相对路径或绝对路径),也可以是字符串

// 字符串
import react from 'react';
// 相对路径
import person from  './person.js';
// 绝对路径
imprt methods from 'src/shard/index.js';

// 只导入不使用
import 'react';

模块导出机制

export

有人可能认为看到模块导入的语法,会立刻联想到export是个对象,export在模块中并不像cjs中是某个对象,而是一种语法,我们去尝试打印export会发现语法错误

console.log(export, '6666'); // 语法错误

export default

export default 与 export的关系

export default 是 export的语法糖,可以简单的理解为export default num是 export const default num的num值浅拷贝以后定义了模块默认导出

export default num; // 可以简单的理解成下面的语法

export const default num; 
// 这句语法会报语法错误,因为default是关键字,不能做变量
import * as object from './export.default.js'; 

console.log(object, 'object')
/* {
  default: 1,
  num: 2,
*/

上面的例子可能不够作证export default 是 export的语法糖,我们接下来看下面的例子

import  { default } from './export.default.js'; 
console.log(num, 'num') // 1

以上例子足以佐证了export default 是 export的语法糖

模块导入机制

导入的变量,不可修改其地址值

import  { num } from './export.default.js';

num = 1; // TypeError: Assignment to constant variable

导入的变量引用的是地址值

export  let num = 1;

export const getNum = () => {
  return num;
}

export const setNum = () => {
  num = num + 1; 
}


export default num;
import { getNum, setNum, num } from './export.default.js';

console.log(num, 'num1')// 1
setNum();


console.log(num, 'num2'); // 2
console.log(getNum(), 'getNum'); // 2

import只能在模块顶层使用

由于import是语法层面的,不涉及代码执行过程中,因此为了编译器的高效率只能在模块顶层使用

 function getName() {
   import * as object1 from './export.default.js';
   /* An import declaration 
     can only be used at the 
     top level of a modul
   */
}

import支持整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个引入对象,所有输出值都加载在这个对象上

import * as object from './export.default.js';

import 是静态加载

import在加载模块是在编译阶段,而非执行阶段,目前没有具体代码能够证明,受限于只能在顶层,下面代码的例证可能不太足

window.obj = import './export.default.js' 
// 该代码的例证点不太足,编译器通不过,放在原理来分析,比较适合

模块机制原理

js内存模型

在讲模块机制之前,我们先来回顾js的内存机制,js内存主要分为 栈内存,堆内存以及常量池(主要存放一些常量如,字符串常量, boolean常量,认为是栈的一部分),至于数字类型分为栈数字,堆数字,存放在堆或者栈中取决于数字的复杂度

由于本章不讲内存存储,不理解的话可以去网上看一些其他的资料。理解了内存关系以后我们就开始继续讲模块的机制原理了

构建模块依赖图

构建ast抽象语法树

以入口节点为根节点(如index.js)创建出一张依赖关系图。不同依赖项之间通过export\import语句来进行关联。

根据上面的ast抽象语法树分析以及提取一些重要信息,我们可以得到以下模块对象

const importEntry = {
  requestModule: ['./counter.js', './display.js'],
  importEntrys: {
    './count.js': {
      moduleRequest: 'count.js',
      importName: 'count',
      importDefault: false
    },
    './display.js': {
      moduleRequest: 'display.js',
      importName: 'render',
      importDefault: false
    }
  }
}

经过抽象语法树分析以后得到的模块与模块之间的关系大致如上面的代码所示

实例化模块

构建完抽象语法树,根据入口文件进行查找并且下载相关的模块形成模块记录,由于浏览器并不能解析所对应的文件需要将其转化为浏览器能够认识的模块

在堆中开辟内存

在堆空间创建一片区域用来放置模块,每有一个模块就便会开辟一块堆内存

模块实例化

根据前面获得的模块进行抽象语法树解析实例化模块,分别将每一个模块进行实例化包括模块导入的地方,