一、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加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。
参考链接: