JavaScript 模块的循环加载

63 阅读2分钟

《用得上的前端知识》系列 - 你我都很忙,能用100字说清楚,绝不写万字长文

CommonJS

CommonJS 的一个模块,就是一个脚本文件。使用 require 命令第一次加载脚本时,会执行整个脚本,然后在内存生成一个对象。结构大致如下:

{
  id: '...',  //模块名
  exports: { ... },  //模块输出的各个接口
  loaded: true,  //表示该模块的脚本是否执行完毕
  ...
}

以后需要用到这个模块的时候,就会到 exports 属性上面取值。即使再次执行 require 命令,也不会再次执行该模块,而是到缓存之中取值(到 exports 上取值)

CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

举个例子:

a.js

// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

上面代码之中,a.js 脚本先输出一个 done 变量,然后加载另一个脚本文件 b.js。注意,此时 a.js 代码就停在这里,等待 b.js 执行完毕,再往下执行。

b.js

// b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

上面代码之中,b.js 执行到第二行,就会去加载 a.js,这时,就发生了"循环加载"。系统会去 a.js 模块对应对象的 exports 属性取值,可是因为 a.js 还没有执行完,从 exports 属性只能取回已经执行的部分,而不是最后的值。

ES6 Module

它遇到模块加载命令 import 时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。并且,变量总是绑定其所在的模块。

所以 ES6 Module 不存在循环加载的问题,只要引用的模块存在,就能正常运行。

比如:

// a.js
import {bar} from './b.js';
export function foo() {
  bar();  
  console.log('执行完毕');
}
foo();

// b.js
import {foo} from './a.js';
export function bar() {  
  if (Math.random() > 0.5) {
    foo();
  }
}

参考资料