CommonJS,AMD,ES6中的Module一次弄懂!你想要的应该都有吧~

1,411 阅读10分钟

模块这块的东西确实该好好理一理,有的知识点总结了以后就不用看了,加油鸭!写了我2天-_-

本文解答这些重点问题:

1.什么是编译时加载什么是运行时加载
2.为什么import要放到顶部
3.打包之后变成了什么 import
4.CMD commonjs module的使用与区别
5.commonjs中使用export和module.exports的区别

1.模块是什么?以及作用

将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来
模块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

作用:
1.避免命名冲突(减少命名空间污染)
2.灵活架构,焦点分离,方便模块间组合、分解 
3.多人协作互不干扰 
4.高复用性和可维护性

2.编译原理

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”。

2.1分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序 var a = 2; 。这段程序通常会被分解成为下面这些词法单元: var 、 a、 = 、 2、; 。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义

2.2解析/语法分析(Parsing)

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。var a = 2; 的抽象语法树中可能会有一个叫作 VariableDeclaration 的顶级节点,接下来是一个叫作 Identifier (它的值是 a )的子节点,以及一个叫作 AssignmentExpression的子节点。 AssignmentExpression 节点有一个叫作 NumericLiteral (它的值是 2 )的子节点

2.3代码生成

将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。抛开具体细节,简单来说就是有某种方法可以将 var a = 2; 的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中。

2.4js与其他语言编译上的区别

比起那些编译过程只有三个步骤的语言的编译器,JavaScript 引擎要复杂得多。例如,在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。

对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种办法(比如 JIT,可以延迟编译甚至实施重编译)来保证性能最佳。

任何 JavaScript 代码片段在执行前都要进行编译(通常就在执行前)。因此,JavaScript 编译器首先会对 var a = 2; 这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它

以上总结:

在js代码执行之前会经历编译阶段(分词/词法分析->解析/语法分析->代码生成),为什么执行之前要编译?因为js语言是高级程序设计语言,容易阅读与编写,而目标语言是机器语言,即二进制代码,能够被计算机直接识别。

3.AMD(require.js)-浏览器

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

3.1 AMD的使用

目录结构

1637635214(1).png

require.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="./require.js" data-main="./main.js" defer async="true" ></script> 
  <!-- 引入require.js包,引入main.js -->
</body>
</html>

main.js

console.log(require, 'main.js开始执行')
require.config({
 baseUrl: "./common/", // 提取公共路径
 paths: {
   "module1": "module1",
   "module2": "module2",
 }
})
require(['module1', 'module2'], function (module1, module2){
 // some code here
  console.log(module1.add(5, 6), 'module1')
  console.log(module2.addUnit(5,6),'module2')
});

module1.js

console.log('module1')
define(function (){
  const add = (x,y) => x+y
  return {
   add
  };
});

module2.js

console.log('module2')
define(['./module1'],function (module){
   const addUnit = (x,y) => `${module.add(x,y)}元` // module2依赖于module1
  return {
      addUnit
  };
});

1637635972(1).png

require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。

具体来说,就是模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。

AMD为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

3.2AMD解决了什么问题

1.在没有AMD的时候,我们用script的src标签引入大量的js,项目越大,引入的js越多,很丑陋,可以说AMD的使用使页面更加美观
2.上面这个案例,module2依赖于module1,那么在require(['module1', 'module2']的时候,我们应该先加载module1再加载module2,这样可以顺序加载
3.defer async="true"可以让js异步加载,避免页面script加载多个js失去响应
4.加载完了直接回调,这样能保证模块已经引入再操作
5.require()实现按需加载,有个功能只有一个页面(单页面某个组件)引入一个库,你不用按需加载就相当于每个页面都要加载这个库

4.CommonJS - 服务端(node.js)

4.1CommonJS的特点

1.CommonJS用于服务端
2.Node.js 模块系统中,每个文件都被视为独立的模块。模块中包括CommonJS规范的核心变量:exports、module.exports、require
3.块化的核心是导出与导入,在Node中通过exports与module.exports负责对模块中的内容进行导出,通过require函数导入其他模块(自定义模块、系统模块、第三方库模块)中的内容

4.2CommonJS基本使用

文件目录

1637639616(1).png

运行node文件只需要cd到同级目录 node+文件名就好

node1.js

const res = require('./node2.js');
console.log(res.obj);

node2.js

exports.obj = {
  text1: '欢迎关注公众号',
  text2: '[程序媛爱唠嗑]',
}

1637640093(1).png

以上是exports + require()使用,下面是module.exports + require()使用

node1.js

const res = require('./node2.js');
console.log(res(5))

node2.js

module.exports = (a) => a + 1

1637647127(1).png

4.3 exports 与 module.exports有什么区别,使用场景

对于这块,很多人瞎写还那么多赞,真的是怕了!所以我的理解如下,有错误望指正!!!

对 module.exports 的引用,其输入更短,它允许一个快捷方式,以便 module.exports.f = ... 可以更简洁地写成 exports.f = ...。 但是,请注意,与任何变量一样,如果将新值分配给 exports,则它就不再绑定到 module.exports

module.exports.hello = true; // 从模块的 require 中导出
exports = { hello: false }; // 未导出,仅在模块中可用

1637648510(1).png

上面是官网的解释,其实就是exports是对 module.exports 的引用,看下面的:

    const module = {
      exports: {}
    }
    const exports = module.exports

    exports.name = '欢迎关注程序媛爱唠嗑' // 虽然是对exports进行赋值,但是调用的仍然是module.exports
    console.log(module.exports, '例子1');  // 这种情况下,是成立的 

db57f1a730ba0703480cee7d22d454a.png

    const module = {
      exports: {}
    }
    const exports = module.exports

    exports = '欢迎关注程序媛爱唠嗑' // 虽然是对exports进行赋值,但是调用的任然是module.exports
    console.log(module.exports, '例子2');  // 这种情况下,exports已经不在对其引用,所以不行

1637648228(1).png

所以总结下:

module.exports在任何的场景下都可用,因为最后还是对module.exports的调用,exports在不用重新赋值的时候用,也就是说module.exports=某某某的时候只能用module.exports,因为用exports相当于切断的引用,只要记住了区别,为了简单,大部分场景还是会能够简写就简写用exports

4.4 CommonJs的特点

1.所有代码都运行在模块作用域,不会污染全局作用域;
2.模块是同步加载的,即只有加载完成,才能执行后面的操作;
3.CommonJS输出是值的拷贝(即,require返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值)。

看例子:

node0.js

const res = require('./node1.js');
console.log(res(4, 5))
console.log('node0')

node1.js


const res = require('./node2.js');
module.exports = (x,y) => `${res(x, y)}元`
console.log('node1')

node2.js

module.exports = (x,y) => x + y
console.log('node2')

1637650398(1).png

node0中有res,node1中有res,并没有影响,说明所有代码都运行在模块作用域,不会污染全局作用域
图中的打印,按照引入的顺序,这也就验证了CommonJS中,模块是同步加载的,即只有加载完成,才能执行后面的操作

第三点CommonJS输出是值的拷贝,也就是说你用require()引入了模块,但是你在最新的模块中怎样去改变,也不会影响你已经require()的模块。见6.1

5. ES6 module ---(浏览器/服务端)

5.1 如何使用

export使用

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script type="module">
    import { text1, text2 } from './module1.js'
    console.log(text1, '-', text2());
    
    // import * as module from './module1.js'  也可以全部引入
    // console.log(module.text1, '-', module.text2());
  </script>
</body>
</html>

module1.js

export const text1 = '欢迎关注公众号';
export const text2 = () => '[程序媛爱唠嗑]';

这里要注意得是: 直接运行的话会出现跨域问题,比如script标签的src就没有跨域问题,所以我们要保证同源,在vscode安装live server,然后运行

image.png

image.png

2.export default module2.js

export default function test() {
  console.log('欢迎关注我的公众号-[程序媛爱唠嗑]');
}

index.html

 <script type="module">
    import test from './module2.js'
    test()
  </script>

1637743270(1).png

5.2 export与export default的区别

1.import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载, 用到export default命令,为模块指定默认输出
2.页面可有多个export但是只能有一个export default

6.CommonJS,AMD,ES6中的Module的区别

    CommonJS 模块的`require()`是同步加载模块,ES6 模块的`import`命令是异步加载,
有一个独立的模块依赖的解析阶段。AMD是异步加载
    CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    CommonJS 模块, AMD是运行时加载,ES6 模块是编译时输出接口。

6.1 CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

其实这里有很多人搞不清楚,我也一样,但是经过一些验证,我理解的是复制一个值的时候,如果是引用数据类型,我们复制的其实是他的内存地址(也就是指针),但是本质上都是叫做值得拷贝,而不是指针的复制,比如字符串没有指针,复制都是复制值,只是看值代表的是什么.

看例子:

node1.js

var mod = require('./node2');
console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

node2.js

var counter = 3
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

1637727620(1).png

上面我引入进来的值就是一个字符串,赋值之后我再对字符串操作是不生效的,这里的值就是一个字符串

等同于:

let a = 1
let b = a
++a
console.log(b); // 1

如果是对象呢? node1.js

var mod = require('./node2');

console.log(mod.counter);  // { count: 3 }
mod.incCounter();
console.log(mod.counter); // { count: 4 }

node2.js

var counter = {
  count: 3
}
function incCounter() {
  counter.count++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

上面我引入进来的值就是一个对象(内存地址),赋值之后我再对对象操作是生效的,这里的值就是一个内存地址

而在ES5module中, module1.js

export let counter = 3;
export function incCounter() {
  counter++;
}

index.html

  <script type="module">
    import { counter, incCounter } from './module1.js';
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4
  </script>

1637728663(1).png

JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

我的理解就是CommonJS输出的值是什么就是对对应值的操作,而es6中的module不管是什么值(引用值还是字符串),都是形成了以key为路径,value为值(不管这个值是字符串还是对象)得map对象,始终输出得是引用,然后在堆中始终指向同一个东西。

具体请查看: 你的 import 被 webpack 编译成了什么?

我也试图查看了webpack编译得源码,感觉自己还没有到解析源码得功力~不过这张图这可以说明点什么.

decec6961ea409aafa207c24d6b9033.png

针对于import的这个特性,我就需要过一个bug,当然是我的锅,当时我在一个js模块中定义了一个数组(我希望初始化是不变的,当前组件的引用改变了模块中的数据,组件切换,数据一直不是初始状态,因为页面并没有重载,我用工厂函数解决了这个问题

回放:

index.html

 <script type="module">
     import { imgs } from './module1.js'
     console.log(imgs);
  </script>

module1.js

export const imgs = [
    {
      img: 'xxx',
      isShow: false,
    },
    {
      img: 'xxx',
      isShow: false,
    },
    {
      img: 'xxx',
      isShow: false,
    },
]

如果你在html中对imgs有修改,是会影响module1的,所以为了避免JSON.parse(JSON.stringify(xx))的使用,我在输出时使用工厂函数返回 index.html

  <script type="module">
     import { imgs } from './module1.js'
     console.log(imgs());

  </script>

module1.js


  export const imgs = () => {
    return [
      {
        img: 'xxx',
        isShow: false,
      },
      {
        img: 'xxx',
        isShow: false,
      },
      {
        img: 'xxx',
        isShow: false,
      },
    ]
  }

6.2 验证import是编译时加载,CommonJS 模块是运行时加载 (面试的时候我被考到过)

从上面2我们已经知道了,在执行代码之前会有一个编译的阶段,也就是要把我们代码解析成计算机认识的语言,CommonJS 模块, AMD是运行时加载,引擎处理import语句是在编译时,如何验证,ES6解决了什么问题

这里先测试ES6 模块

module1.js

export default function test() {
  console.log('欢迎关注我的公众号-[程序媛爱唠嗑]-小橘子');
}

module2.js

export default function test() {
  console.log('欢迎关注我的公众号-[程序媛爱唠嗑]-大橘子');
}

index.html

 <script type="module">
    const box = 1
    if(box === 1) {
      import test from './module1.js'
    } else {
      import test from './module2.js'
    }
    test()
  </script>

1637745429(1).png

报错了,对import进行编译,因为它们用到了表达式、变量和if结构。在静态分析阶段(编译阶段),这些语法都是没法得到值的.所以证明了引擎处理import语句是在编译时

看看CommonJS

node2.js

export default function test() {
  console.log('欢迎关注我的公众号-[程序媛爱唠嗑]-大橘子');
}

node1.js

export default function test() {
  console.log('欢迎关注我的公众号-[程序媛爱唠嗑]-小橘子');
}

node0.js

const box = 1
let res
if (box === 1) {
  res = require('./node1.js');
} else {
  res = require('./node2.js');
}
console.log(res());

1637745992(1).png

证明了 CommonJS实在运行时加载

6.3 为什么要把import提到最前面

因为在引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。也就是说,importexport命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。

大白话理解就是假设一个羽毛球桶里面有很多五颜六色羽毛球,必须先把蓝色先挑出来(编译),再考虑下一步操作(执行)是不是在你放羽毛球的时候把蓝色的放上面,这样就能提高效率了~我的理解哈

6.4 引擎处理import语句是在编译时有什么好处?

ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。

所谓静态分析就是不执行代码,从字面量上对代码进行分析,ES6之前的模块化,比如我们可以动态require一个模块,只有执行后才知道引用的什么模块,这个就不能通过静态分析去做优化。

这是 ES6 modules 在设计时的一个重要考量,也是为什么没有直接采用 CommonJS,正是基于这个基础上,才使得 tree-shaking 成为可能,这也是为什么 rollup 和 webpack 2 都要用 ES6 module syntax 才能 tree-shaking。

CommonJS从6.1的例子可以看出,我在运行时,可以去决定引入什么样的模块,所以动态的引入没法去做静态分析,而ES6中的module在编译阶段就确定了,可以进行分析.

7.参考与引用

require.js了解一下
ES6
你不知道的javscript上册
如有遗漏,我可以补上