跳到主要内容

模块化

模块化是一种软件设计方法,旨在将程序划分为独立且可重用的模块。在 JavaScript 中,模块化可以帮助开发者将代码分割成更小的文件单元,并使得代码更易于维护、重用和扩展。

MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules

1.CommonJS

CommonJS 是一种模块化规范,最初是为服务器端 JavaScript(如 Node.js)设计的。它使用 require() 导入模块,module.exports 导出模块。

const vue = require('vue')

if(isDev) {
// 可以动态引入,执行时引入
const app = require('./app')
}
// 导出模块
module.exports = someFunction;

2.AMD (Asynchronous Module Definition)

AMD 是一种异步模块加载规范,主要用于浏览器端的模块化开发。它使用 define() 定义模块,require() 异步加载模块。

// 定义模块
define(['module1', 'module2'], function(module1, module2) {
return someFunction;
});

// 异步加载模块
require(['module1', 'module2'], function(module1, module2) {
// 执行回调函数
});

3.ES Module (ECMAScript 2015)

ES6 引入了原生的模块化支持,使用 import 和 export 关键字导入和导出模块。

// 导入模块
import module1 from './module1';
import { module2 } from './module2';


if(isDev) {
// 1. 编译时报错,只能静态引入
import app from './app'
// 2. 但是可以这样使用(动态模块加载)
import("/modules/mymodule.js").then((module) => {
// 它返回一个 promise
// Do something with the module.
});
}
// 导出模块
export default someFunction;

4.UMD (Universal Module Definition)

UMD(Universal Module Definition)是一种通用的模块定义规范,旨在解决 JavaScript 模块化在不同环境(浏览器、Node.js 等)下的兼容性问题。UMD 可以兼容 CommonJS、AMD 和全局对象(Global Object)这三种模块定义方式,使得模块可以在不同环境中使用。

<script src="calculator.js"></script>
<script>
// 使用 calculator 模块提供的功能
console.log(calculator.add(5, 3)); // 输出: 8
console.log(calculator.subtract(10, 4)); // 输出: 6
</script>
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 兼容
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS 兼容
module.exports = factory();
} else {
// 全局对象兼容
root.calculator = factory();
}
})(window, function () { // window全局对象
// 模块代码
return {
add: function (a, b) {
return a + b;
},
subtract: function (a, b) {
return a - b;
}
};
});

5.commonJs 和 es6 模块差异

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

在 CommonJS 模块系统中,每次导入模块时都会得到模块的一个拷贝,而不是直接引用模块中的值。因此,如果在一个模块中修改了模块导出的对象,不会影响其他模块对该对象的引用。这意味着每个模块都有自己的私有拷贝,修改一个模块中的值不会影响其他模块。

// counter.js
let count = 0;

module.exports = {
count,
getCount: function() {
return count;
},
increment: function() {
count++;
}
};

// main.js
const counter = require('./counter')

// 每次导入模块时都会得到模块的一个拷贝
console.log('count', counter.count) // 0
console.log(counter.getCount()) // 0
// 1
counter.increment()
console.log('count', counter.count) // 0
console.log(counter.getCount()) // 1
// 2
counter.increment()
console.log('count', counter.count) // 0
console.log(counter.getCount()) // 2
// 3
counter.count = 100
counter.increment()
console.log('count', counter.count) // 100
console.log(counter.getCount()) // 3

而在 ES6 模块系统中,导入的是模块的引用,而不是拷贝。这意味着当一个模块被导入到另一个模块时,它们实际上引用的是同一个对象。因此,如果在一个模块中修改了导入的对象,其他导入了相同对象的模块也会受到影响,因为它们引用的是同一个对象。

// counter.js
let count = 0;

export const getCount = () => count;
export const increment = () => count++;
export { count }; // 将 count 暴露出去

// main.js
import { getCount, increment, count } from './counter';

console.log('count', count) // 0
console.log(getCount()) // 0
// 1
increment()
console.log('count', count) // 1
console.log(getCount()) // 1
// 2
count = 100
console.log('count', count) // 100
console.log(getCount()) // 100
// moduleA.js
export let count = 0;
setTimeout(() => {
count = 10;
}, 1000)

// moduleB.js
import { count } from 'moduleA'

console.log(count); // 0
setTimeout(() => {
console.log(count); // 10
}, 2000)
结论:
1. 在 2000ms 后再去打印 count 的确是会变化,你会发现 count 变成了 10,这也意味着 ES Module 导出的时候并不会用快照,而是从引用中来获取值。
2. 而在 CommonJS 中则完全相反,CommonJS 中两次都输出了 0,这意味着 CommonJS 导出的是快照。

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

// commonjs
if(isDev) {
// 可以动态引入,执行时引入
const app = require('./app')
}

// es module
if(isDev) {
// 1. 编译时报错,只能静态引入
import app from './app'
// 2. 但是可以这样使用(动态模块加载)
import("/modules/mymodule.js").then((module) => {
// 它返回一个 promise
// Do something with the module.
});
}

3. CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

  1. 同步加载:CommonJS 模块的 require() 方法是同步加载模块的,即在执行 require() 时,会立即加载模块并且阻塞后续代码的执行,直到模块加载完成才继续执行后续代码。这意味着在 CommonJS 中,模块的加载是同步的,导致在加载大型模块时可能会阻塞整个应用程序的执行。
// commonjsModule.js
const fs = require('fs');

console.log('CommonJS module is being executed');
console.log('Reading file synchronously...');
const content = fs.readFileSync('example.txt', 'utf-8');
console.log('File content:', content);
console.log('CommonJS module execution finished');
  1. 异步加载:ES6 模块的 import 命令是异步加载模块的。当使用 import 命令加载模块时,它会返回一个 Promise 对象,该 Promise 在模块加载完成后被解析为一个包含导入模块所有导出内容的对象。因此,在 ES6 中,模块的加载是异步的,不会阻塞后续代码的执行。
// es6Module.js
console.log('ES6 module is being executed');
console.log('Importing file asynchronously...');
import('example.txt').then(content => {
console.log('File content:', content.default);
});
console.log('ES6 module execution finished');
  • commonjsModule.js 使用 CommonJS 模块加载文件内容。它使用 require() 同步加载文件内容,因此会阻塞后续代码的执行,直到文件内容加载完成。
  • es6Module.js 使用 ES6 模块加载文件内容。它使用 import() 异步加载文件内容,因此不会阻塞后续代码的执行,文件内容加载完成后会执行 .then() 中的回调函数。

6. require与import的区别

  • require支持动态导入,import() 也可以支持
  • require是 同步导入,import属于 异步 导入
  • require是值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化

7. Dynamic Import

tc39: https://github.com/tc39/proposal-dynamic-import

const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
link.addEventListener("click", e => {
e.preventDefault();
// 使用方式
import(`./section-modules/${link.dataset.entryModule}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
});
}

// 实现原理
// 使用 HTML <script type="module">,以下代码将提供与 类似的功能import():
function importModule(url) {
// 本质上返回一个 promise
return new Promise((resolve, reject) => {
const script = document.createElement("script");
const tempGlobal = "__tempModuleLoadingVariable" + Math.random().toString(32).substring(2);
script.type = "module";
script.textContent = `import * as m from "${url}"; window.${tempGlobal} = m;`;

script.onload = () => {
resolve(window[tempGlobal]);
delete window[tempGlobal];
script.remove();
};

script.onerror = () => {
reject(new Error("Failed to load module script with URL " + url));
delete window[tempGlobal];
script.remove();
};

document.documentElement.appendChild(script);
});
}

8. 模块循环引用

循环引用是指两个或多个模块相互引用彼此的情况,形成一个循环链。在 JavaScript 中,循环引用可能会导致一些问题,特别是在模块系统中,它可能会导致模块加载的死锁或内存泄漏。

造成的问题

  1. 加载死锁:如果两个模块相互依赖,并且其中一个模块的加载依赖于另一个模块的加载,那么就可能导致加载死锁。这是因为模块 A 在加载时需要模块 B,而模块 B 在加载时又需要模块 A,因此两个模块相互等待对方加载完成,导致加载过程无法完成。
// moduleA.js
const moduleB = require('./moduleB');

console.log('moduleA is being executed');

const a = {
value: 1,
b: moduleB.b
};

module.exports.a = a;

// moduleB.js
const moduleA = require('./moduleA');

console.log('moduleB is being executed');

const b = {
value: 2,
a: moduleA.a
};

module.exports.b = b;
  1. 内存泄漏:循环引用可能导致内存泄漏,因为两个相互引用的对象会在垃圾回收时无法被释放。即使这些对象不再被程序直接引用,它们之间的循环引用也会阻止垃圾回收器将它们释放,从而导致内存泄漏。

解决方法

  1. 重构代码:重新设计模块之间的依赖关系,避免循环引用的发生。可能需要将部分逻辑移动到新的模块中,或者将循环引用拆解为单向引用。

  2. 延迟加载:在需要时再加载模块,而不是在模块定义时立即加载。这可以通过将模块加载逻辑放置在函数内部或使用动态导入等方式来实现。

  3. 使用中介者模式:在循环引用的模块之间引入一个中介者模块,用于协调它们之间的通信,从而避免直接相互引用。

  4. 手动解除引用:在不需要使用模块时,手动将模块的引用置为 null,以帮助垃圾回收器识别和释放循环引用的对象。