„Zirkuläre Abhängigkeit“ bedeutet, dass die Ausführung von Skript a von Skript b abhängt und die Ausführung von Skript b von Skript a abhängt.
// a.js var b = require('b'); // b.js var a = require('a');
Normalerweise weist „Schleifenladen“ auf das Vorhandensein einer starken Kopplung hin. Wenn es nicht richtig gehandhabt wird, kann es auch zu rekursivem Laden führen, wodurch das Programm nicht ausgeführt werden kann. Daher sollte es vermieden werden.
Tatsächlich ist dies jedoch schwer zu vermeiden, insbesondere bei großen Projekten mit komplexen Abhängigkeiten. Es ist leicht, dass a von b, b bis c und c von a abhängt. Dies bedeutet, dass der Modullademechanismus „Loop-Loading“-Situationen berücksichtigen muss.
In diesem Artikel wird erläutert, wie die JavaScript-Sprache mit dem „Schleifenladen“ umgeht. Derzeit verfügen die beiden gängigsten Modulformate CommonJS und ES6 über unterschiedliche Verarbeitungsmethoden und liefern unterschiedliche Ergebnisse.
1. Ladeprinzip des CommonJS-Moduls
Bevor wir vorstellen, wie ES6 das „Schleifenladen“ handhabt, stellen wir zunächst das Ladeprinzip des beliebtesten CommonJS-Modulformats vor.
Ein Modul von CommonJS ist eine Skriptdatei. Wenn der Befehl require das Skript zum ersten Mal lädt, führt er das gesamte Skript aus und generiert dann ein Objekt im Speicher.
{ id: '...', exports: { ... }, loaded: true, ... }
Im obigen Code ist das id-Attribut des Objekts der Modulname, das exports-Attribut ist jede vom Modul ausgegebene Schnittstelle und das geladene Attribut ist ein boolescher Wert, der angibt, ob das Skript des Moduls ausgeführt wurde. Es gibt viele weitere Attribute, die hier jedoch weggelassen werden. (Eine ausführliche Einführung finden Sie unter „require() Quellcode-Interpretation“.)
Wenn Sie dieses Modul in Zukunft verwenden müssen, erhalten Sie den Wert aus dem Exportattribut. Selbst wenn der Befehl require erneut ausgeführt wird, wird das Modul nicht erneut ausgeführt, sondern der Wert wird aus dem Cache abgerufen.
2. Schleifenladen des CommonJS-Moduls
Eine wichtige Funktion des CommonJS-Moduls ist die Ausführung beim Laden, d. h. der gesamte Skriptcode wird bei Bedarf ausgeführt. Der Ansatz von CommonJS besteht darin, dass, sobald ein Modul „in einer Schleife geladen“ ist, nur der ausgeführte Teil ausgegeben wird und der nicht ausgeführte Teil nicht ausgegeben wird.
Werfen wir einen Blick auf die Beispiele in der offiziellen Dokumentation. Der Code der Skriptdatei a.js lautet wie folgt.
exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 执行完毕');
Im obigen Code gibt das a.js-Skript zuerst eine fertige Variable aus und lädt dann eine andere Skriptdatei b.js. Beachten Sie, dass der a.js-Code zu diesem Zeitpunkt hier stoppt und darauf wartet, dass b.js die Ausführung abschließt, und dann die Ausführung fortsetzt.
Sehen Sie sich den b.js-Code noch einmal an.
exports.done = false; var a = require('./a.js'); console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 执行完毕');
Wenn im obigen Code b.js bis zur zweiten Zeile ausgeführt wird, wird a.js geladen. Zu diesem Zeitpunkt erfolgt das „Schleifenladen“. Das System erhält den Wert des Exportattributs des Objekts, das dem a.js-Modul entspricht. Da a.js jedoch noch nicht ausgeführt wurde, kann nur der ausgeführte Teil vom Exportattribut abgerufen werden, nicht der endgültige Wert.
Der ausgeführte Teil von a.js hat nur eine Zeile.
exports.done = false;
Daher wird für b.js nur eine Variable eingegeben, die aus a.js stammt, und der Wert ist falsch.
Dann wird b.js weiter ausgeführt. Wenn alle Ausführungen abgeschlossen sind, wird das Ausführungsrecht an a.js zurückgegeben. Daher wird a.js so lange ausgeführt, bis die Ausführung abgeschlossen ist. Wir schreiben ein Skript main.js, um diesen Prozess zu überprüfen.
var a = require('./a.js'); var b = require('./b.js'); console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
Führen Sie main.js aus und die Ergebnisse sind wie folgt.
$ 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
Der obige Code beweist zwei Dinge. Erstens wurde in b.js nicht a.js ausgeführt, sondern nur die erste Zeile. Zweitens: Wenn main.js bis zur zweiten Zeile ausgeführt wird, wird b.js nicht erneut ausgeführt, sondern das zwischengespeicherte Ausführungsergebnis von b.js wird ausgegeben, dh die vierte Zeile.
exports.done = true;
3. Schleifenladen von ES6-Modulen
Der Betriebsmechanismus von ES6-Modulen unterscheidet sich von CommonJS. Wenn der Modulladebefehl importiert wird, wird das Modul nicht ausgeführt, sondern nur eine Referenz generiert. Warten Sie, bis Sie es wirklich benötigen, und rufen Sie dann den Wert im Modul ab.
Daher sind ES6-Module dynamische Referenzen, es gibt kein Problem beim Zwischenspeichern von Werten und die Variablen im Modul sind an das Modul gebunden, in dem sie sich befinden. Bitte sehen Sie sich das Beispiel unten an.
// 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);
Im obigen Code ist die Variable foo von m1.js gleich bar, wenn sie zum ersten Mal geladen wird. Nach 500 Millisekunden wird sie wieder gleich baz.
Mal sehen, ob m2.js diese Änderung richtig lesen kann.
$ babel-node m2.js bar baz
Der obige Code zeigt, dass das ES6-Modul die laufenden Ergebnisse nicht zwischenspeichert, sondern dynamisch den Wert des geladenen Moduls erhält und die Variable immer an das Modul gebunden ist, in dem sie sich befindet.
Dies führt dazu, dass ES6 das „Schleifenladen“ wesentlich anders handhabt als CommonJS. ES6 kümmert sich überhaupt nicht darum, ob ein „Schleifenladen“ auftritt, es generiert lediglich einen Verweis auf das geladene Modul. Der Entwickler muss sicherstellen, dass der Wert abgerufen werden kann, wenn der Wert tatsächlich abgerufen wird.
Bitte sehen Sie sich das folgende Beispiel an (Auszug aus „Exploring ES6“ von 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》中的一节。