背景
JS モジュール性の進化には、初期のCommonJSから、その後のAMDとCMD、そして今日のES6モジュラーソリューションに至るまで、長いプロセスを経てきたことは誰もが知っています。JS 言語では適者生存、サーバーサイドのモジュラー ソリューションCommonJSが生き残り、ES6 によって導入されたモジュラー ソリューションは誰もが認めるようになり、将来的には JS の主要なモジュラー ソリューションになる可能性があります。誰もが次のような疑問を抱いたことがあると思います。どちらのモジュール式ソリューションも誰もが使用でき、認識できるため、それぞれの利点は何ですか? 2 つのモジュール式ソリューションの違いは何ですか? 疑問点のある記事もいくつか読みましたが、まとめがあまり包括的ではなかったので、両者の違いをしっかりまとめた記事を書きたいと思いました。
本文に入る前にもう 1 つの質問がありますが、これらはすべて JS であるのに、なぜ 2 つのモジュール ソリューションを使用する必要があるのでしょうか?
ブラウザでも CommonJS を使用してみてはいかがでしょうか?
この質問に答える前に、CommonJS の構文は同期であるという事実をまず明確にする必要がありますrequire
。 を使用してrequire
モジュールをロードする場合、次のコードを実行する前にモジュールがロードされるまで待機する必要があります。この事実を知っていれば、私たちの質問は簡単に答えることができます。NodeJSはサーバーです。require
構文を使用してモジュール (通常はファイル) をロードします。必要なのはローカル ハードディスクからファイルを読み取るだけであり、その速度は比較的高速です。しかし、ブラウザ側では異なります。通常、ファイルはサーバーまたは CDN に保存されます。モジュールのロードに同期方式を使用する場合、速度はネットワークによって決定される必要があり、時間がかかる場合があります。このように、ブラウザは簡単に「一時停止状態」に入る可能性があります。そのため、後続のAMDおよびCMDモジュラー ソリューションが用意されており、これらは非同期で読み込まれ、ブラウザー側での使用により適しています。
さて、最初の質問を解決したら、本題に入りましょう。
2 つの重要な違い
これらの違いについては、多かれ少なかれ聞いたことがあると思いますが、結局のところ、私たちは皆、日々の開発においてこれらの違いに対処しなければなりません。実際、両者の最大の違いは次の 2 つです。
- CommonJS モジュールは値のコピーを出力し、ES6 モジュールは値への参照を出力します。
- CommonJS モジュールは実行時にロードされ、ES6 モジュールはコンパイル時に出力インターフェイスになります。
まずは最初の違いを見てみましょう。
CommonJS は値のコピーを出力します。つまり、値が出力されると、モジュール内でその後変更されても、この値の外部での使用には影響しません。具体的な例:
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
次に、このモジュールを他のファイルで使用します。
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上の例は、counter
変数を外部に出力した場合、後でモジュールの内部incCounter
メソッドを呼び出してその値を変更したとしても、その値は変わらないことを十分に示しています。
ES6 モジュールの動作メカニズムはまったく異なり、JS エンジンがスクリプトを静的に解析し、モジュール読み込みコマンドに遭遇するとimport
、読み取り専用の参照を生成します。スクリプトが実際に実行されると、この読み取り専用参照に基づいて、ロードされたモジュールから値が取得されます。
// lib.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
上記のコードは、 ES6 モジュールimport
の変数がcounter
変更可能であり、lib.js
変数が配置されているモジュール内の変更を完全に反映していることを示しています。
2 番目の違いは、ES6 モジュールが非常に人気がある最大の理由の 1 つです。CommonJS が実際にオブジェクトをロードすることはわかっています。このオブジェクトはスクリプトの実行時にのみ生成され、一度だけ生成されます。これについては後ほど詳しく説明します。ただし、ES6 モジュールはオブジェクトではありません。その外部インターフェイスは単なる静的な定義であり、静的コード分析段階で生成されます。このようにして、さまざまなツールを使用して JS モジュールの依存関係を分析し、JS モジュールの最適化を行うことができます。コード。
循環依存性
ループ読み込みとは、a
スクリプトがb
スクリプトに依存し、b
スクリプトの実行がa
スクリプトに依存することを意味します。大規模なプロジェクトでは、一般に依存関係が複雑で、循環依存関係が発生しやすいため、モジュール型ソリューションの場合は、この状況を考慮する必要があります。
CommonJS ループ読み込み
CommonJS の循環読み込みの問題を理解するには、まずその読み込み原理を一般的に理解する必要があります。CommonJS モジュールは通常ファイルであり、reqiure
モジュールが初めてロードされると、メモリ内にオブジェクトが生成されます。おそらく次のようになります。
{
id: '...',
exports: { ... },
loaded: true,
...
}
上の例では、モジュール名とモジュールが出力する各インターフェイスid
という、モジュールが実行されたかどうかを示すいくつかの主要な属性のみをリストしました。今後このモジュールが使用されるとき、値はこのオブジェクトのプロパティから直接取得されます。モジュールのコマンドを複数回実行した場合でも、そのコマンドは最初にロードされたときに 1 回だけ実行され、キャッシュが手動でクリアされない限り、それ以降はキャッシュから読み取られます。exports
loaded
exports
require
CommonJS モジュールの特徴はロード時に実行されることであり、スクリプトがロードされるとreqiure
完全に実行されます。モジュールを「ループロード」すると、実行された部分のみが出力され、未実行の部分は出力されません。公式の例を見てみましょう。最初に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 执行完毕');
上記のコードは、最初にdone
変数を出力し、次にロードを開始しますb.js
。a.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 执行完毕');
同様にa.js
、b.js
変数をエクスポートした後、2 行目からロードが開始されa.js
、循環依存関係が生じます。次に、システムはメモリ オブジェクトにアクセスして変数の値exports
を取得しますが、実行が完了していないため、出力されたばかりの値のみを取得します。その後、次のコードを実行し続け、実行が完了すると、実行権が残りのコードの実行に戻ります。このプロセスを確認するには、新しいプロセスを作成します。done
a.js
false
b.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
結果は次のようになります。
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
CommonJS モジュールは循環ロードに遭遇するため、すべてのコードが実行された後の値ではなく、現在実行されている部分の値を出力するため、この 2 つの間に違いが生じる可能性があります。したがって、変数を入力するときは十分に注意する必要があります。
ES6 でのループ読み込み
ES6 モジュールは動的参照であるため、 を使用してimport
変数をロードした場合、変数はキャッシュされず、実際に値を取得するときに最終的な値が取得されます。次の例を見てください。
// 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
関数内にeven
パラメータがありn
、それが 0 でない限り、1 が減算されてロードされますodd()
。odd.js
同様の操作も行います。
上記のコードを実行すると、結果は次のようになります。
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
上記のコードでは、パラメーターが10 から 0 に変更されるn
と、even()
合計 6 回実行されるため、変数はcounter
6 になります。even.js
この例では、モジュール内の変更により、出力されるカウンタ変数の値が変化することがわかります。
2 つのモジュラー ソリューションの読み込み方法が異なるため、循環読み込みの処理方法が異なります。
他に何が違うのでしょうか?
もちろん、それらの間には他にもいくつかの違いがあります。それらをここに直接リストします。まず第一に、これはthis
ES6 モジュールのトップレベルをthis
指すキーワードでありundefined
、CommonJS モジュールのトップレベルはthis
現在のモジュールを指します。次に、CommonJS モジュールは ES6 モジュールに直接ロードできますが、単一の出力項目ではなく、全体としてのみロードできます。
// 正确
import packageMain from 'commonjs-package';
// 报错
import { method } from 'commonjs-package';
Node.js による ES6 モジュールの処理はさらに面倒です。Node.js には ES6 モジュール形式と互換性のない独自の CommonJS モジュール仕様があるからです。現在、2 つのモジュール ソリューションは個別に処理されており、バージョン v13.2 以降、Node.js はデフォルトで ES6 モジュールのサポートを有効にしています。NodeJS では、ES6 モジュールがサフィックス ファイル名を使用する必要があります。NodeJS が最後にファイルにmjs
遭遇する限り、それは ES6 モジュールとみなされます。mjs
ファイル接尾辞の変更に加えて、もちろんプロジェクトpackage.json
ファイル内のtype
フィールドを指定することもできますmodule
。
{
"type": "module"
}
ただし、require
このコマンドは.mjs
ファイルをロードできず、エラーが報告されます。ファイルをimport
ロードできるのはコマンドのみです。.mjs
逆に、コマンドは.mjs
ファイル内で使用できず、使用する必要があるため、日常の開発では ES6 モジュールと CommonJS モジュールを混在させないように注意してください。require
import
要約する
今回で、この記事は基本的に終わりです。この記事の内容を要約してみましょう。
- CommonJSの構文は
require
同期であるため、 CommonJSモジュール仕様はサーバー側での使用にのみ適していますが、ES6 モジュールはブラウザー側とサーバー側の両方で使用できます。一部の特別ルールはのみ使用できます。 - CommonJSモジュールは値のコピーを出力し、ES6 モジュールは値への参照を出力します。
- CommonJSモジュールは実行時にロードされますが、ES6 モジュールはコンパイル時に出力インターフェイスとなるため、JS モジュールを静的に分析できます。
- 2 つのモジュールの読み込みメカニズムが異なるため、周期的な読み込みを処理するときの動作が異なります。CommonJS は循環依存関係に遭遇した場合、実行された部分のみを出力し、その後の出力や変更は出力された変数には影響しません。逆に、ES6 モジュールは
import
変数のロードを使用し、変数はキャッシュされず、値が実際に取得されるときに最終値を取得できます。 this
モジュールのトップレベルでのポインティングの問題に関しては、 CommonJSのトップレベルでは現在のモジュールを指しますがthis
、ES6 モジュールではthis
を指しますundefined
。- 2 つのモジュールが相互に参照する問題に関しては、ES6 モジュールはCommonJSモジュールのロードをサポートしています。ただし、CommonJS
require
では ES6 モジュールをサポートできず、NodeJS では 2 つのモジュール ソリューションは別々に処理されます。