"Circular dependency" means that the execution of script a depends on script b, and the execution of script b depends on script a.
// a.js var b = require('b'); // b.js var a = require('a');
Usually, "loop loading" indicates the existence of strong coupling. If not handled well, it may also lead to recursive loading, making the program unable to execute, so it should be avoided.
But in fact, this is difficult to avoid, especially for large projects with complex dependencies. It is easy for a to depend on b, b to c, and c to depend on a. This means that the module loading mechanism must take into account "loop loading" situations.
This article introduces how the JavaScript language handles "loop loading". Currently, the two most common module formats, CommonJS and ES6, have different processing methods and return different results.
1. Loading principle of CommonJS module
Before introducing how ES6 handles "loop loading", let's first introduce the loading principle of the most popular CommonJS module format.
A module of CommonJS is a script file. The first time the require command loads the script, it will execute the entire script and then generate an object in memory.
{ id: '...', exports: { ... }, loaded: true, ... }
In the above code, the id attribute of the object is the module name, the exports attribute is each interface output by the module, and the loaded attribute is a Boolean value, indicating whether the script of the module has been executed. There are many other attributes, but they are omitted here. (For detailed introduction, please refer to "require() Source Code Interpretation".)
When you need to use this module in the future, you will get the value from the exports attribute. Even if the require command is executed again, the module will not be executed again, but the value will be retrieved from the cache.
2. Loop loading of CommonJS module
An important feature of the CommonJS module is execution when loading, that is, all script code will be executed when required. CommonJS's approach is that once a module is "loop loaded", only the executed part will be output, and the unexecuted part will not be output.
Let’s take a look at the examples in the official documentation. The code of the script file a.js is as follows.
exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 执行完毕');
In the above code, the a.js script first outputs a done variable, and then loads another script file b.js. Note that the a.js code stops here at this time, waiting for b.js to complete execution, and then continues execution.
Look at the b.js code again.
exports.done = false; var a = require('./a.js'); console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 执行完毕');
In the above code, when b.js is executed to the second line, a.js will be loaded. At this time, "loop loading" occurs. The system will get the value of the exports attribute of the object corresponding to the a.js module. However, because a.js has not yet been executed, only the executed part can be retrieved from the exports attribute, not the final value.
The executed part of a.js has only one line.
exports.done = false;
Therefore, for b.js, it only inputs one variable done from a.js, and the value is false.
Then, b.js continues to execute. When all executions are completed, the execution right is returned to a.js. Therefore, a.js continues to execute until the execution is completed. We write a script main.js to verify this process.
var a = require('./a.js'); var b = require('./b.js'); console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
Execute main.js and the results are as follows.
$ 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
The above code proves two things. First, in b.js, a.js has not been executed, only the first line has been executed. Second, when main.js is executed to the second line, b.js will not be executed again, but the cached execution result of b.js will be output, that is, its fourth line.
exports.done = true;
3. Loop loading of ES6 modules
The operating mechanism of ES6 modules is different from CommonJS. When it encounters the module loading command import, it will not execute the module, but only generate a reference. Wait until you really need to use it, then get the value in the module.
Therefore, ES6 modules are dynamic references, there is no problem of caching values, and the variables in the module are bound to the module in which they are located. Please see the example below.
// 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);
In the above code, the variable foo of m1.js is equal to bar when it is first loaded. After 500 milliseconds, it becomes equal to baz again.
Let’s see if m2.js can read this change correctly.
$ babel-node m2.js bar baz
The above code shows that the ES6 module does not cache the running results, but dynamically obtains the value of the loaded module, and the variable is always bound to the module in which it is located.
This causes ES6 to handle "loop loading" essentially differently from CommonJS. ES6 doesn't care at all whether "loop loading" occurs, it just generates a reference to the loaded module. The developer needs to ensure that the value can be obtained when the value is actually obtained.
Please see the following example (excerpted from "Exploring ES6" by Dr. Axel Rauschmayer).
// 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加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。
我们再来看ES6模块加载器SystemJS给出的一个例子。
// even.js import { odd } from './odd' export var counter = 0; export function even(n) { counter++; return n == 0 || odd(n - 1); } // odd.js import { even } from './even'; export function odd(n) { return n != 0 && even(n - 1); }
上面代码中,even.js里面的函数foo有一个参数n,只要不等于0,就会减去1,传入加载的odd()。odd.js也会做类似操作。
运行上面这段代码,结果如下。
$ babel-node > import * as m from './even.js'; > m.even(10); true > m.counter 6 > m.even(20) true > m.counter 17
上面代码中,参数n从10变为0的过程中,foo()一共会执行6次,所以变量counter等于6。第二次调用even()时,参数n从20变为0,foo()一共会执行11次,加上前面的6次,所以变量counter等于17。
这个例子要是改写成CommonJS,就根本无法执行,会报错。
// even.js var odd = require('./odd'); var counter = 0; exports.counter = counter; exports.even = function(n) { counter++; return n == 0 || odd(n - 1); } // odd.js var even = require('./even').even; module.exports = function(n) { return n != 0 && even(n - 1); }
上面代码中,even.js加载odd.js,而odd.js又去加载even.js,形成"循环加载"。这时,执行引擎就会输出even.js已经执行的部分(不存在任何结果),所以在odd.js之中,变量even等于null,等到后面调用even(n-1)就会报错。
$ node > var m = require('./even'); > m.even(10) TypeError: even is not a function
[说明] 本文是我写的《ECMAScript 6入门》第20章《Module》中的一节。