开始

  1. CommonJS(常用在服务器端,同步的,如nodejs)
  2. AMD(常用在浏览器端,异步的,如requirejs)(Asynchronous Module Definition)
  3. CMD(常用在浏览器端,异步的,如seajs
  4. UMD(AMD&& CommonJS) 这些模块化规范的核心价值都是让 JavaScript 的模块化开发变得简单和自然。

image.png
服务器端模块 在服务器端,所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。
浏览器端模块: 在浏览器端,所有的模块都放在服务器端,同步加载,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。因此,浏览器端的模块,不能采用”同步加载”(synchronous),只能采用”异步加载“(asynchronous)。

模块循环加载问题

CommonJS 模块的加载原理

CommonJS 的每一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

  1. {
  2. id: '...',
  3. exports: { ... },
  4. loaded: true,
  5. ...
  6. }

上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。
以后需要用到这个模块的时候,就会到**exports**属性上面取值。即使再次执行**require**命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

ES6 模块的循环加载

ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

  1. // a.mjs
  2. import {bar} from './b';
  3. console.log('a.mjs');
  4. console.log(bar);
  5. export let foo = 'foo';
  6. // b.mjs
  7. import {foo} from './a';
  8. console.log('b.mjs');
  9. console.log(foo);
  10. export let bar = 'bar';

上面代码中,a.mjs加载b.mjsb.mjs又加载a.mjs,构成循环加载。执行a.mjs,结果如下。

  1. $ node --experimental-modules a.mjs
  2. b.mjs
  3. ReferenceError: foo is not defined

让我们一行行来看,ES6 循环加载是怎么处理的。

  1. 首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs
  2. 接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。
  3. 执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。

解决这个问题的方法,就是让**b.mjs**运行的时候,**foo**已经有定义了。这可以通过将**foo**写成函数来解决。

  1. // a.mjs
  2. import {bar} from './b';
  3. console.log('a.mjs');
  4. console.log(bar());
  5. function foo() { return 'foo' }
  6. export {foo};
  7. // b.mjs
  8. import {foo} from './a';
  9. console.log('b.mjs');
  10. console.log(foo());
  11. function bar() { return 'bar' }
  12. export {bar};

这时再执行a.mjs就可以得到预期结果。

  1. $ node --experimental-modules a.mjs
  2. b.mjs
  3. foo
  4. a.mjs
  5. bar

这是因为函数具有提升作用,在执行**import {bar} from './b'**时,函数**foo**就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。

  1. // a.mjs
  2. import {bar} from './b';
  3. console.log('a.mjs');
  4. console.log(bar());
  5. const foo = () => 'foo';
  6. export {foo};

上面代码的第四行,改成了函数表达式,就不具有提升作用,执行就会报错。

参考

  1. Module 的加载实现
  2. 前端科普系列(3):CommonJS 不是前端却革命了前端
  3. webpack模块化原理-commonjs
  4. webpack模块化原理-ES module
  5. 前端模块化开发解决方案详解