liangbus / blogging

Blog to go
10 stars 0 forks source link

CommonJS,AMD 和 ES6 模块化的差异 #17

Open liangbus opened 4 years ago

liangbus commented 4 years ago

上述三者是我们听得最多的模块化方式,此外还有 CMD,UMD 这些,比较小众的,本次就不单独拿出来说了

模块化,相信任何人多多少少都有接触过,项目到了一定程度,都需要将其拆分成不同的模块,用以解决命名冲突,文件依赖相关问题,因而就催生了上述多种的模块化标准,在 ES6 之前,JavaScript 一直都没有一个官方的模块化标准,而社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种,前者用于服务器,后者用于浏览器。直到 ES6 的推出,终于在语言层面上实现了实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块化与 CommonJS 和 AMD 最大的不同就是

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

其中 nodeJs 采用的就是 CommonJs 规范

CommonJS

nodeJs 是 CommonJs 规范的主要实践者,它有4个重要的环境变量为模块化实现提供支持: module, exports, require, global。 实际使用时,推荐使用 module.exports 来输出模块接口,(阮一峰老师规范教程也是不推荐直接使用 exports)

  • CommonJS规范加载模块是同步的,只有加载完成,才能执行后面的操作
  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。

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

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。

// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3

上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。

liangbus commented 4 years ago

AMD

由于 NodeJs 主要用于服务端,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑异步加载的方式,所以 CommonJS 规范比较适用 但是如果是在客户端(浏览器)环境,要从服务端加载模块,这时就必须采用异步的加载模式,因此客户端一般使用 AMD 规范,require.js 就是以 AMD 规范实现的。示例:

/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});

引用模块的时候,我们将模块名放在 [] 中作为 require 的第一参数,如果 我们定义的模块本身也依赖其他模块,那就需要将它们放在 [] 作为 define 的第一参数,示例:

// 定义math.js模块
define(function() {
    var baseNum = 0
    var add = function(x, y) {
        return x + y
    }
    return {
        add,
        baseNum
    }
})
// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
  var classify = function(list){
    _.countBy(list,function(num){
      return num > 30 ? 'old' : 'young';
    })
  };
  return {
    classify :classify
  };
})

// 父文件
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
  var sum = math.add(10,20);
  $("#sum").html(sum);
});
liangbus commented 4 years ago

ES6 module

ES6 在语言层面实现了模块化标准(但目前浏览器不能直接识别),目的是成为浏览器和服务器通用的模块解决方案。 其模块主要由两个关键字命令组成: export: 模块输出接口 import: 模块引入接口 与 CommonJS 运行时加载不同,ES6 module 是编译时加载,所以在编译时就会确认好模块的依赖关系,也正因此可以对其做一些静态分析的工作 比如下面代码,fs 中定义了很多属性和方法,CommonJS 会把整个 fs 加载进来作为对象

// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

ES6 module 只会加载需要的属性和方法

// ES6模块
import { stat, exists, readFile } from 'fs';

结论:

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

  • CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值(仅限基本类型,如果是对象,还是可以改变的)。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

  • 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
  • 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。

参考: Module 的加载实现 CommonJS规范 - JavaScript 标准参考教程