JS模块的循环引用

一、node.js循环引用

CommonJS有一个特性就是:运行时加载,引入一个模块,会加载整个模块;与ES6不同的是 编译时加载,

可以指定加载的部分,不会全部加载。

若一旦出现了循环引用,则只输出已经执行的部分,还未执行的部分不会输出。当第二次加载模块,也不会执行改模块,而是优先从缓存中取值

例如:

a.js

exports.done = false
var b = require('./b.js')
console.log('在a.js中,b.done ='+ 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 b = require('./a.js')
console.log('在b.js中,a.done ='+ a.done)
exports.done = true
console.log('b.js执行完毕')

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

a.js已经执行的部分,只有一行。

exports.done = false;

因此,对于b.js来说,它从a.js只输入一个变量done,值为false。

然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。我们写一个脚本main.js,验证这个过程。

var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

执行main.js,运行结果如下。

$ node main.js

在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true

上面的代码证明了两件事。

一是,在b.js之中,a.js没有执行完毕,只执行了第一行。

二是,优先从缓存中取值,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。

exports.done = true;

总之,CommonJS 输入的是被输出值的拷贝,不是引用。

另外,由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。

var a = require('a'); // 安全的写法
var foo = require('a').foo; // 危险的写法

exports.good = function (arg) {
  return a.foo('good', arg); // 使用的是 a.foo 的最新值
};

exports.bad = function (arg) {
  return foo('bad', arg); // 使用的是一个部分加载时的值
};

上面代码中,如果发生循环加载,require('a').foo的值很可能后面会被改写,改用require('a')会更保险一点。

再看一个示例

(1)a.js文件
var b = require('../test/b');
module.exports.a = 1;
console.log('a.js get b:' + b.b);
(2)b.js文件
var a = require('../test/a');
console.log('b.js get a:' + a.a);
module.exports.b = 2;

运行 a.js文件,输出结果如下

b.js get a:undefined
a.js get b:2

二、ES6 import循环引用

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用,等到真正的需要用到时,再到模块中取值。

ES6模块是动态引用,不存在缓存值的问题。

// m1.js
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);

// m2.js
import {foo} from './m1.js';
console.log(foo);
setTimeout(() => console.log(foo), 500);

上面代码中,m1.js的变量foo,在刚加载时等于bar,过了500毫秒,又变为等于baz。

让我们看看,m2.js能否正确读取这个变化。

$ babel-node m2.js

bar
baz

上面代码表明,ES6模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块。

这导致ES6处理"循环加载"与CommonJS有本质的不同。ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

// 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();
  }
}

按照CommonJS规范,上面的代码是没法执行的。a先加载b,然后b又加载a,这时a还没有任何执行结果,所以输出结果为null,即对于b.js来说,变量foo的值等于null,后面的foo()就会报错。

但是,ES6可以执行上面的代码。

$ babel-node a.js

执行完毕

a.js之所以能够执行,原因就在于ES6加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。

参考链接:

JavaScript 模块的循环加载

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容