阅读 101

梳理JS中的CommonJS、AMD和CMD

CommonJS规范

NodeJs应用模块化使用的是CommonJS,每一个文件就是一个模块,拥有自己的作用域、变量以及方法,对其他的模块不可见。 CommoJS规范加载模块是同步的。

从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。但是要求ES6模块采用.mjs后缀名。

module对象

NodeJs在运行某些模块的时会自动创建一个module对象,同时会给module对象添加一个exports对象,初始化的值为{}

module.exports = {}
复制代码

image.png NodeJs在模块里可以直接调用exports和module.exports两个全局变量,但是exports只是module.exports的一个引用。

exports的使用

// add.js
function add(a, b){
    return a + b;
}
exports.plus = plus;
其实等价于
module.exports = {
    plus: plus
}

// 在main.js中引用add.js
let Add = require('add.js')
Add.add(1, 2) // 3
复制代码

module.exports的使用

// add.js
function add(a, b){
    return a + b;
}
module.exports = add;

// 在main.js中引用add.js
let add = require('add.js')
add(1, 2) // 3
复制代码

require语法的加载机制

NodeJs中第一次加载某个模块时,NodeJs会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性,不存在动态更新

这里有几个需要注意的点:

  • exports只是module对象中exports方法的引用
  • require引用模块后,返回的是module.exports而不是exports

ES6中的模块化(import、export、export default)

在一个文件中,export、import可以有多个,但是export default只能有一个。

export的描述及使用

export命令规定的是对外的接口,必须于模块内部的变量建立一一对应关系。export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取得模块内部的实时的值。,也就是说,当导入对象在模块内值发生变化后,import导入的对象也会相应的同步变化。

// add.js导出
export var 变量 = 值
export { 变量 }
export { 变量 as 输出的变量 }
// 引用
import { 变量 } from 'add.js' 
复制代码

export default(默认导出)的语法

上面的export是动态绑定关系。import default却不是动态的。默认导出的导出结果是值而不是引用。 原因是默认导出可以看作一种对“default赋值”的特例,本质上是一种赋值,所以拿到是值而不是引用。

// add.js导出
export default {
    add(){ }
}
export default var count = `1`
// 引用
import add from 'add.js'
复制代码

只要是使用export default导出的都是值而不是引用!除了以下的特例:

export default function load() {
    console.log('doing something')
}
复制代码

export default function 是一种特例,这种写法会导致导出的是引用而不是值。 如果我们用正常的方式导出Function,那么导出的还是值,哪怕导出的对象是个function。

function onload() {}

export default onload;
复制代码

import命令和动态import

import命令输入的变量都是只读的,因为他的本质是输入接口。不允许在加载模块的脚本里面,改写接口。

标准用法的import导入的模块是静态的,会是所有被导入的模块,在加载时就被编译。在有些场景中,我们希望根据条件导入模块或者按需导入模块,我们可以使用动态导入代替静态导入。 关键字import可以像调用函数一样来动态导入模块。以这种方式,将返回一个promise。

// add.js
export let value = 'oldValue';

import('./add.js').then((module) => {
    // Do something with the module.
})
// 或者是
let module = await import('./add.js')
let { value } = await import('./add.js')

console.log(module.value)
console.log(value)
复制代码

通过上文我们知道, export导出的是引用。那么我们在使用import命令行导入export导出的对象一定是引用吗?

答案是否定的。对于导入来说,{} = await import() 相当于重新赋值,所以具体对象的引用会丢失,也就是说异步的导入会重新赋值。而 let module = await import()引用不变的原因是module本身是一个对象,module.value的引用还是不变的,即便module是被重新赋值的。

AMD规范

基于CommonJS规范的NodeJS出来以后,服务端的模块概念已经形成,很自然地,大家就想要客户端模块。CommonJS规范因为同步加载的,如果加载的资源较大就会导致加载时间过长,那么整个应用就会停在那里等待。这对于服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待的时间就是硬盘的读取时间。

但是对于浏览器来说却是一个大问题,因为模块都放在服务器端,等待的时间取决于网速的快慢,可能要等很长的时间,所以浏览器端不能使用“同步加载”。

为了解决这种问题,只能采用异步加载。这就是AMD规范诞生的背景。AMD定义了一套JavaScript模块依赖异步加载标准,来解决同步加载的问题。

AMD是依赖前置的,换句话说,再解析和执行当前模块之前,模块作者必须执行当前模块所以来的模块,表现在require函数的调用结构上为:

define(['./a', './b'], function(a, b) => { 
    a.doSomething(); 
    b.doSomething(); 
})
复制代码

举例: 模块通过define函数定义在闭包中,格式如下

define(id?: string, dependencies?: string[], factory: Function | Object)
复制代码

dependencies指定了所要依赖的模块列表,它是一个数组,也是可选的参数,每个依赖的模块的输出将作为参数一次传入factory中。如果没有指定dependencies,那么它的默认值是["require", "exports", "module"]

举例: 定义一个myModule的模块,它依赖jQuery模块。使用define和require需要引入require.js文件,文件地址: requirejs.org/docs/releas…

<scritpt src='./require.js' ></script>
define('myModule', ['jquery'], function($) {
    $('body').text('hello world');
    return true;
})
// 使用
require(['myModule'], function(myModule) {
    console.log(myModule) // true
})
复制代码

需要注意的是,异步加载依赖项应使用数组来列出依赖项

require(['myModule']) //true
require('myModule') // false
复制代码

目前,主要要两个JavaScript库实现了AMD规范:require.js和curl.js

CMD规范

CMD是seajs推崇的规范,CMD则是依赖就近,用的时候再require。格式如下

define(id?: string, dependencies?: string[], factory: Function | Object)
复制代码

CMD和AMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。

AMD依赖前置,js可以方便知道依赖模块是谁,立即加载。

CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块。

参考文章: 精读《默认、命名导出的区别》

文章分类
前端
文章标签