モジュールロードの実装

前の章ではモジュールの構文を紹介しましたが、この章では、ES6 モジュールをブラウザーや Node.js にロードする方法と、実際の開発でよく発生するいくつかの問題 (ループロードなど) を紹介します。

ブラウザの読み込み中

従来の手法

HTML Web ページでは、ブラウザは <script> タグを通じて JavaScript スクリプトを読み込みます。

<!-- ページに埋め込まれたスクリプト -->
<script type="アプリケーション/javascript">
  // モジュールコード
</script>

<!-- 外部スクリプト -->
<script type="application/javascript" src="path/to/myModule.js">
</script>

上記コードでは、ブラウザスクリプトのデフォルト言語はJavaScriptなので、type="application/javascript"は省略可能です。

デフォルトでは、ブラウザは JavaScript スクリプトを同期的に読み込みます。つまり、レンダリング エンジンは <script> タグに遭遇すると停止し、スクリプトが実行されるまで待機し、その後下向きにレンダリングを続けます。外部スクリプトの場合は、スクリプトのダウンロード時間も追加する必要があります。

スクリプトのサイズが大きい場合、ダウンロードと実行に時間がかかるため、ブラウザが詰まり、ユーザーはブラウザが応答せずに「スタック」しているように感じます。これは明らかに非常に悪いエクスペリエンスであるため、ブラウザではスクリプトの非同期読み込みが許可されています。非同期読み込み用の 2 つの構文を次に示します。

<script src="path/to/myModule.js" 遅延></script>
<script src="path/to/myModule.js" 非同期></script>

上記のコードで、<script> タグが defer または async 属性をオンにすると、スクリプトは非同期でロードされます。レンダリング エンジンは、このコマンド行を検出すると、外部スクリプトのダウンロードを開始しますが、ダウンロードされて実行されるのを待たずに、次のコマンドを直接実行します。

deferasync の違いは次のとおりです。 defer は、ページ全体がメモリ内で正常にレンダリングされるまで (DOM 構造が完全に生成され、他のスクリプトが実行されるまで) 実行されません。レンダリング エンジン このスクリプトの実行後、レンダリングは中断されます。つまり、「defer」は「レンダリング後に実行」、「async」は「ダウンロード後に実行」を意味します。さらに、複数の「defer」スクリプトがある場合、それらはページに表示される順序でロードされますが、複数の「async」スクリプトはロード順序を保証できません。

ルールのロード

ブラウザは ES6 モジュールをロードし、<script> タグも使用しますが、type="module" 属性を追加します。

<script type="module" src="./foo.js"></script>

上記のコードは、モジュール「foo.js」を Web ページに挿入します。「type」属性が「module」に設定されているため、ブラウザはこれが ES6 モジュールであることを認識します。

ブラウザは、type="module" を指定した <script> を非同期的にロードし、ブラウザをブロックしません。つまり、モジュール スクリプトを実行する前にページ全体がレンダリングされるまで待機することは、<script > を開くことと同じです。 ` タグの属性。

<script type="module" src="./foo.js"></script>
<!-- --> と同等
<script type="module" src="./foo.js" 遅延></script>

Web ページに複数の <script type="module"> がある場合、それらはページ上に表示される順序で実行されます。

このとき、<script> タグの async 属性をオンにすることもできます。このとき、読み込みが完了していれば、レンダリング エンジンはレンダリングを中断してすぐに実行します。実行が完了したら、レンダリングを再開します。

<script type="module" src="./foo.js" 非同期></script>

async 属性を使用すると、<script type="module"> はページに表示される順序では実行されず、モジュールがロードされるとすぐに実行されます。

ES6 モジュールでは Web ページに埋め込むこともでき、構文上の動作は外部スクリプトを読み込む場合とまったく同じです。

<script type="モジュール">
  「./utils.js」からユーティリティをインポートします。

  //その他のコード
</script>

たとえば、jQuery はモジュールの読み込みをサポートしています。

<script type="モジュール">
  import $ from "./jquery/src/jquery.js";
  $('#message').text('jQuery からこんにちは!');
</script>

外部モジュール スクリプト (上記の例では「foo.js」) については、注意すべき点がいくつかあります。

  • コードはグローバル スコープではなくモジュール スコープで実行されます。モジュール内のトップレベル変数は外部からは見えません。
  • モジュールスクリプトは、use strict が宣言されているかどうかに関係なく、自動的に strict モードを採用します。
  • モジュール間では、import コマンドを使用して他のモジュールをロードすることができます (.js 接尾辞は省略できず、絶対 URL または相対 URL を指定する必要があります)、または export コマンドを使用できます。外部インターフェースを出力します。
  • モジュールでは、トップレベルの this キーワードは window を指すのではなく unknown を返します。言い換えれば、モジュールのトップレベルで this キーワードを使用しても意味がありません。
  • 同じモジュールが複数回ロードされた場合、実行されるのは 1 回だけです。

以下はモジュールの例です。

https://example.com/js/utils.js」からユーティリティをインポートします。

定数 x = 1;

console.log(x === window.x); //false
console.log(this === 未定義); // true

最上位の「this」が「unknown」に等しいという構文ポイントを使用すると、現在のコードが ES6 モジュール内にあるかどうかを検出できます。

const isNotModuleScript = this !== 未定義;

ES6 モジュールと CommonJS モジュールの違い

ES6 モジュールをロードする Node.js について説明する前に、ES6 モジュールが CommonJS モジュールとは完全に異なることを理解することが重要です。

それらには 3 つの大きな違いがあります。

  • CommonJS モジュールは値のコピーを出力し、ES6 モジュールは値への参照を出力します。
  • CommonJS モジュールは実行時にロードされ、ES6 モジュールはコンパイル時に出力インターフェイスになります。
  • CommonJS モジュールの require() はモジュールを同期的にロードしますが、ES6 モジュールの import コマンドは非同期的にロードされ、モジュールの依存関係については独立した解決フェーズが行われます。

2 番目の違いは、CommonJS がオブジェクト (つまり、module.exports プロパティ) をロードするためです。このオブジェクトは、スクリプトの実行後にのみ生成されます。 ES6 モジュールはオブジェクトではありません。その外部インターフェイスは、コードの静的分析フェーズ中に生成される単なる静的な定義です。

以下では、最初の違いを中心に説明します。

CommonJS モジュールは値のコピーを出力します。つまり、値が出力されると、モジュール内の変更は値に影響を与えません。次のモジュール ファイル「lib.js」の例を見てください。

//lib.js
var カウンタ = 3;
関数 incCounter() {
  カウンタ++;
}
module.exports = {
  カウンター: カウンター、
  incCounter: incCounter,
};

上記のコードは、内部変数 counter と、この変数をオーバーライドする内部メソッド incCounter を出力します。次に、このモジュールを「main.js」にロードします。

// メイン.js
var mod = require('./lib');

console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3

上記のコードは、「lib.js」モジュールがロードされた後、その内部変更が出力「mod.counter」に影響しないことを示しています。これは、mod.counter がプリミティブな値であり、キャッシュされるためです。関数として書かないと内部で変更された値を取得できません。

//lib.js
var カウンタ = 3;
関数 incCounter() {
  カウンタ++;
}
module.exports = {
  カウンタを取得() {
    返却カウンター
  }、
  incCounter: incCounter,
};

上記のコードでは、出力 counter プロパティは実際には valuer 関数です。ここで、main.js を再度実行すると、内部変数 counter の変更を正しく読み取ることができます。

$ノードmain.js
3
4

ES6 モジュールは CommonJS とは動作が異なります。 JS エンジンがスクリプトを静的に分析し、モジュール読み込みコマンド import を検出すると、読み取り専用の参照が生成されます。スクリプトが実際に実行されると、この読み取り専用参照に基づいて、ロードされたモジュールから値が取得されます。言い換えれば、ES6 の import は、Unix システムの「シンボリック リンク」に似ています。元の値が変更されると、import によってロードされる値も変更されます。したがって、ES6 モジュールは動的参照であり、モジュール内の変数は、それらが配置されているモジュールにバインドされません。

上の例を見てみましょう。

//lib.js
エクスポートレットカウンタ = 3;
エクスポート関数 incCounter() {
  カウンタ++;
}

// メイン.js
import { counter, incCounter } から './lib';
コンソール.ログ(カウンター); // 3
incCounter();
コンソール.ログ(カウンター); // 4

上記のコードは、ES6 モジュールによって入力された変数 counter が生きており、それが配置されているモジュール lib.js 内の変更を完全に反映していることを示しています。

「エクスポート」セクションにある別の例を見てみましょう。

// m1.js
エクスポート var foo = 'バー';
setTimeout(() => foo = 'baz', 500);

// m2.js
'./m1.js' から {foo} をインポートします。
コンソール.log(foo);
setTimeout(() => console.log(foo), 500);

上記のコードでは、「m1.js」の変数「foo」は、最初にロードされたときは「bar」と等しくなりますが、500 ミリ秒後には「baz」と等しくなります。

m2.js がこの変更を正しく読み取れるかどうかを見てみましょう。

$ babel-node m2.js

バー
バズ

上記のコードは、ES6 モジュールが実行結果をキャッシュせず、ロードされたモジュールの値を動的に取得し、変数が常にその変数が配置されているモジュールにバインドされていることを示しています。

ES6 によってインポートされたモジュール変数は単なる「シンボリック リンク」であるため、この変数は読み取り専用であり、再割り当てするとエラーが発生します。

//lib.js
エクスポート let obj = {};

// メイン.js
'./lib' から { obj } をインポートします。

obj.prop = 123;
obj = {}; // タイプエラー

上記のコードでは、main.jslib.js から変数 obj を入力します。 obj に属性を追加することはできますが、再代入するとエラーが報告されます。変数 obj が指すアドレスは読み取り専用で再割り当てできないため、main.jsobj という名前の const 変数を作成するのと似ています。

最後に、export はインターフェイスを通じて同じ値を出力します。さまざまなスクリプトがこのインターフェイスをロードし、同じインスタンスを取得します。

// mod.js
関数 C() {
  this.sum = 0;
  this.add = function () {
    this.sum += 1;
  };
  this.show = function () {
    console.log(this.sum);
  };
}

エクスポート let c = new C();

上記のスクリプト mod.jsC のインスタンスを出力します。さまざまなスクリプトがこのモジュールをロードし、同じインスタンスを取得します。

//x.js
{c} を './mod' からインポートします。
c.add();

//y.js
{c} を './mod' からインポートします。
c.show();

// メイン.js
インポート './x';
インポート './y';

ここで「main.js」を実行すると、出力は「1」になります。

$ babel-node main.js
1

これは、「x.js」と「y.js」の両方が「C」の同じインスタンスをロードすることを証明します。

Node.jsモジュールの読み込み方法

概要

JavaScript には現在 2 種類のモジュールがあります。 1 つは ES6 モジュール (ESM と呼ばれます)、もう 1 つは CommonJS モジュール (CJS と呼ばれます) です。

CommonJS モジュールは Node.js に固​​有であり、ES6 モジュールとは互換性がありません。構文の点で、この 2 つの最も明らかな違いは、CommonJS モジュールは require()module.exports を使用し、ES6 モジュールは importexport を使用することです。

これらは異なる読み込みスキームを使用します。 Node.js v13.2 以降、Node.js はデフォルトで ES6 モジュールのサポートを有効にしています。

Node.js では、ES6 モジュールが「.mjs」接尾辞のファイル名を使用する必要があります。つまり、スクリプト ファイルで「import」または「export」コマンドが使用されている限り、「.mjs」接尾辞を使用する必要があります。 Node.js は .mjs ファイルを検出すると、それを ES6 モジュールと見なし、デフォルトで strict モードを有効にします。各モジュール ファイルの先頭に "use strict" を指定する必要はありません。

サフィックス名を .mjs に変更したくない場合は、プロジェクトの package.json ファイルで type フィールドを module として指定できます。

{
   "タイプ": "モジュール"
}

セットアップが完了すると、プロジェクトの JS スクリプトは ES6 モジュールとして解釈されます。

# ES6モジュールに解釈する
$ ノード my-app.js

この時点でも CommonJS モジュールを使用したい場合は、CommonJS スクリプトのサフィックス名を .cjs に変更する必要があります。 「type」フィールドがない場合、または「type」フィールドが「commonjs」の場合、「.js」スクリプトは CommonJS モジュールとして解釈されます。

一文に要約すると、「.mjs」ファイルは常に ES6 モジュールとしてロードされ、「.cjs」ファイルは常に CommonJS モジュールとしてロードされ、「.js」ファイルのロードは、 package.json

ES6 モジュールと CommonJS モジュールを混合しないように注意してください。 require コマンドは .mjs ファイルをロードできず、import コマンドのみが .mjs ファイルをロードできます。エラーが報告されます。逆に、「.mjs」ファイルでは「require」コマンドは使用できず、「import」を使用する必要があります。

package.json のメインフィールド

package.json ファイルには、モジュールのエントリ ファイルを指定する 2 つのフィールド、mainexports があります。比較的単純なモジュールの場合は、「main」フィールドを使用して、モジュールによってロードされるエントリ ファイルを指定するだけです。

// ./node_modules/es-module-package/package.json
{
  "タイプ": "モジュール",
  "メイン": "./src/index.js"
}

上記のコードは、プロジェクトのエントリ スクリプトが ./src/index.js であり、その形式が ES6 モジュールであることを指定しています。 「type」フィールドがないと、「index.js」は CommonJS モジュールとして解釈されます。

次に、「import」コマンドでこのモジュールをロードできます。

// ./my-app.mjs

import { 何か } から 'es-module-package';
// 実際にロードされるのは ./node_modules/es-module-package/src/index.js

上記のコードでは、スクリプトの実行後、Node.js は ./node_modules ディレクトリに移動し、es-module-package モジュールを探して、そのモジュールの main フィールドに基づいてエントリを実行します。モジュール「package.json」ドキュメント。

このとき、CommonJS モジュールの require() コマンドを使用して es-module-package モジュールをロードすると、CommonJS モジュールが export コマンドを処理できないため、エラーが報告されます。

package.json のフィールドをエクスポートします

「exports」フィールドは「main」フィールドよりも高い優先順位を持っています。多くの用途があります。

(1) サブディレクトリのエイリアス

「package.json」ファイルの「exports」フィールドでは、スクリプトまたはサブディレクトリのエイリアスを指定できます。

// ./node_modules/es-module-package/package.json
{
  "エクスポート": {
    "./submodule": "./src/submodule.js"
  }
}

上記のコードは、src/submodule.jssubmodule としてエイリアス化されていることを指定しており、このファイルはエイリアスからロードできるようになります。

「es-module-package/submodule」からサブモジュールをインポートします。
// ./node_modules/es-module-package/src/submodule.js をロードします

以下はサブディレクトリのエイリアスの例です。

// ./node_modules/es-module-package/package.json
{
  "エクスポート": {
    "./features/": "./src/features/"
  }
}

「es-module-package/features/x.js」から機能をインポートします。
// ./node_modules/es-module-package/src/features/x.js をロードします

エイリアスが指定されていない場合、「モジュール+スクリプト名」の形式でスクリプトをロードすることはできません。

// エラーを報告する
「es-module-package/private-module.js」からサブモジュールをインポートします。

// エラーを報告しません
'./node_modules/es-module-package/private-module.js' からサブモジュールをインポートします。

(2)メインの別名

exports フィールドのエイリアスが . である場合、これはモジュールのメイン エントリを表し、main フィールドよりも高い優先順位を持ち、exports フィールドの値に直接短縮できます。

{
  "エクスポート": {
    ".": "./main.js"
  }
}

// と同等
{
  "エクスポート": "./main.js"
}

「exports」フィールドは ES6 をサポートする Node.js でのみ認識されるため、「main」フィールドと組み合わせることで、古いバージョンの Node.js と互換性を持たせることができます。

{
  "メイン": "./main-legacy.cjs",
  "エクスポート": {
    ".": "./main-modern.cjs"
  }
}

上記のコードでは、古いバージョンの Node.js (ES6 モジュールをサポートしていない) のエントリ ファイルは「main-legacy.cjs」で、新しいバージョンの Node.js のエントリ ファイルは「main-modern」です。 .cjs`。

(3) 条件付き負荷

. エイリアスを使用すると、ES6 モジュールと CommonJS に異なるエントリを指定できます。

{
  "タイプ": "モジュール",
  "エクスポート": {
    ".": {
      "require": "./main.cjs",
      "デフォルト": "./main.js"
    }
  }
}

上記のコードでは、エイリアス .require 条件は require() コマンドのエントリ ファイル (つまり CommonJS のエントリ) を指定し、 default 条件はその他の状況のエントリを指定します。 (つまり、ES6 のエントリー)。

上記の記述は次のように省略できます。

{
  "エクスポート": {
    "require": "./main.cjs",
    "デフォルト": "./main.js"
  }
}

同時に他のエイリアスがある場合、省略形は使用できません。省略形を使用しないとエラーが報告されることに注意してください。

{
  // エラーを報告する
  "エクスポート": {
    "./feature": "./lib/feature.js",
    "require": "./main.cjs",
    "デフォルト": "./main.js"
  }
}

CommonJS モジュールは ES6 モジュールをロードします

CommonJS の require() コマンドは ES6 モジュールをロードできず、import() メソッドを使用してのみロードできます。

(async () => {
  await import('./my-app.mjs');
})();

上記のコードは CommonJS モジュールで実行できます。

require() が ES6 モジュールをサポートしない理由の 1 つは、ES6 モジュールが同期的にロードされ、トップレベルの await コマンドが ES6 モジュール内で使用される可能性があるため、同期ロードに失敗することです。

ES6 モジュールが CommonJS モジュールをロードしています

ES6 モジュールの「import」コマンドは CommonJS モジュールをロードできますが、単一の出力項目だけではなく、全体としてのみロードできます。

// 正しい
「commonjs-package」から packageMain をインポートします。

// エラーを報告する
'commonjs-package' から { メソッド } をインポートします。

これは、ES6 モジュールは静的コード解析をサポートする必要があり、CommonJS モジュールの出力インターフェイスが module.exports であり、オブジェクトであるため静的に解析できないため、全体としてのみロードできるためです。

単一の出力項目をロードするには、次のように記述できます。

「commonjs-package」から packageMain をインポートします。
const {メソッド} = パッケージメイン;

Node.js の組み込み module.createRequire() メソッドを使用する代替の読み込み方法もあります。

//cjs.cjs
module.exports = 'cjs';

//esm.mjs
import { createRequire } from 'モジュール';

const require = createRequire(import.meta.url);

const cjs = require('./cjs.cjs');
cjs === 'cjs' // true

上記のコードでは、ES6 モジュールは module.createRequire() メソッドを通じて CommonJS モジュールをロードできます。ただし、この書き方はES6とCommonJSを混ぜることに相当するため、推奨されません。

両方の形式のモジュールを同時にサポート

モジュールが CommonJS 形式と ES6 形式の両方をサポートすることも簡単です。

元のモジュールが ES6 形式の場合、import() を使用して CommonJS をロードできるように、export default obj などの全体的な出力インターフェイスを指定する必要があります。

元のモジュールが CommonJS 形式の場合、ラッピング レイヤーを追加できます。

'../index.js' から cjsModule をインポートします。
エクスポート const foo = cjsModule.foo;

上記のコードは、まず CommonJS モジュール全体を入力し、次に必要に応じて名前付きインターフェイスを出力します。

このファイルのサフィックスを .mjs に変更することも、それをサブディレクトリに配置してから、このサブディレクトリに別の package.json ファイルを配置し、{ type: "module" } を指定することもできます。

別の方法は、「package.json」ファイルの「exports」フィールドで 2 つの形式モジュールのそれぞれの読み込みエントリを指定することです。

「エクスポート」:{
  "require": "./index.js""インポート": "./esm/wrapper.js"
}

上記のコードでは require()import を指定しており、モジュールをロードすると自動的に別のエントリ ファイルに切り替わります。

Node.js の組み込みモジュール

Node.js の組み込みモジュールは、全体として読み込むことも、指定した出力項目として読み込むこともできます。

//フルロード
「イベント」から EventEmitter をインポートします。
const e = new EventEmitter();

//指定された出力項目を読み込みます
import { readFile } から 'fs';
readFile('./foo.txt', (err、ソース) => {
  if (エラー) {
    コンソール.エラー(エラー);
  } それ以外 {
    コンソール.ログ(ソース);
  }
});

ロードパス

ES6 モジュールのロード パスにはスクリプトのフル パスを指定する必要があり、スクリプトのサフィックスは省略できません。 「import」コマンドと「package.json」ファイルの「main」フィールドが省略されている場合、エラーが報告されます。

// ES6 モジュールでエラーが報告されます
import { 何か } から './index';

ブラウザの「インポート」読み込みルールに一致するように、Node.js の「.mjs」ファイルは URL パスをサポートしています。

import './foo.mjs?query=1'; // ./foo をロードし、パラメータ ?query=1 を渡します。

上記のコードでは、スクリプト パスにパラメータ ?query=1 があり、Node は URL ルールに従ってそれを解釈します。パラメータが異なる限り、同じスクリプトが複数回ロードされ、異なるキャッシュに保存されます。このため、ファイル名に「:」、「%」、「#」、「?」などの特殊文字が含まれる場合は、これらの文字をエスケープすることをお勧めします。

現在、Node.js の import コマンドは、ローカル モジュール (file: プロトコル) と data: プロトコルのロードのみをサポートしており、リモート モジュールのロードはサポートしていません。さらに、スクリプト パスは相対パスのみをサポートし、絶対パス (つまり、「/」または「//」で始まるパス) はサポートしません。

内部変数

ES6 モジュールはユニバーサルである必要があり、同じモジュールを変更せずにブラウザ環境とサーバー環境の両方で使用できます。この目標を達成するために、Node.js では CommonJS モジュールに固有の一部の内部変数を ES6 モジュールでは使用できないと規定しています。

まず、「this」というキーワードがあります。 ES6 モジュールでは、トップレベルの thisunknown を指しますが、CommonJS モジュールでは、トップレベルの this は現在のモジュールを指します。これが 2 つの大きな違いです。

次に、次のトップレベル変数は ES6 モジュールには存在しません。

  • 引数
  • 「必要」
  • モジュール
  • 「輸出」
  • __ファイル名
  • __ディレクトリ名

ループ読み込み

「循環依存」とは、「a」スクリプトの実行が「b」スクリプトに依存し、「b」スクリプトの実行が「a」スクリプトに依存することを意味します。

//a.js
var b = require('b');

// b.js
var a = require('a');

一般に、「循環ロード」は強い結合が存在することを示しており、適切に処理しないと再帰ロードが発生し、プログラムが実行できなくなる可能性があるため、回避する必要があります。

しかし実際には、これを避けるのは難しく、特に複雑な依存関係を持つ大規模なプロジェクトでは、「a」が「b」に依存し、「b」が「c」に依存し、「c」が「`」に依存することが容易に起こります。ああ。これは、モジュール読み込みメカニズムが「ループ読み込み」状況を考慮する必要があることを意味します。

JavaScript 言語の場合、現在最も一般的な 2 つのモジュール形式である CommonJS と ES6 は、「ループ読み込み」を異なる方法で処理し、異なる結果を返します。

CommonJS モジュールのロード原理

ES6 が「ループ読み込み」をどのように処理するかを紹介する前に、まず最も一般的な CommonJS モジュール形式の読み込み原理を紹介しましょう。

CommonJSのモジュールはスクリプトファイルです。 「require」コマンドが初めてスクリプトをロードするとき、スクリプト全体が実行され、メモリ内にオブジェクトが生成されます。

{
  ID: '...'、
  エクスポート: { ... }、
  ロード済み: true、
  ...
}

上記のコードは、モジュールを内部的にロードした後に Node によって生成されるオブジェクトです。このオブジェクトの id 属性はモジュール名、 exports 属性はモジュールによって出力される各インターフェイス、 loaded 属性はモジュールのスクリプトが実行されたかどうかを示すブール値です。他にもたくさんの属性がありますが、ここでは省略します。

将来このモジュールを使用する必要がある場合は、「exports」属性から値を取得します。再度requireコマンドを実行してもモジュールは再実行されませんが、値はキャッシュから取得されます。つまり、CommonJS モジュールが何回ロードされても、最初にロードされたときに 1 回だけ実行され、後でロードされた場合は、システム キャッシュがない限り、最初の実行の結果が返されます。手動でクリアされます。

CommonJS モジュールの循環ロード

CommonJS モジュールの重要な機能はロード時の実行です。つまり、「require」が実行されるとすべてのスクリプト コードが実行されます。モジュールを「ループロード」すると、実行された部分のみが出力され、未実行の部分は出力されません。

Node 公式ドキュメント の例を見てみましょう。スクリプトファイル「a.js」のコードは以下のとおりです。

エクスポート.done = false;
var b = require('./b.js');
console.log('a.js では、b.done = %j', b.done);
エクスポート.done = true;
console.log('a.js の実行が完了しました');

上記のコードでは、a.js スクリプトは最初に done 変数を出力し、次に別のスクリプト ファイル b.js を読み込みます。 a.js コードはこの時点でここで停止し、続行する前に b.js が実行されるのを待つことに注意してください。

もう一度「b.js」のコードを見てください。

エクスポート.done = false;
var a = require('./a.js');
console.log('b.js では、a.done = %j', a.done);
エクスポート.done = true;
console.log('b.js の実行が完了しました');

上記コードでは、2行目まで「b.js」を実行すると、「a.js」が読み込まれます。このとき「ループロード」が発生します。システムは「a.js」モジュールに対応するオブジェクトの「exports」属性の値を取得しますが、「a.js」はまだ実行されていないため、「exports」から取得できるのは実行された部分だけです。 ` 属性であり、最終的な値ではありません。

a.js の実行部分は 1 行だけです。

エクスポート.done = false;

したがって、b.js の場合、a.js から値 false を持つ変数 done を 1 つだけ入力します。

その後、「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を実行すると以下のようになります。

$ノードmain.js

b.js では、a.done = false
b.jsの実行が完了しました
a.js では、b.done = true
a.jsの実行が完了しました
main.js では、a.done=true、b.done=true

上記のコードは 2 つのことを証明しています。まず、「b.js」では「a.js」は実行されておらず、最初の行だけが実行されています。次に、main.js を 2 行目まで実行すると、再び b.js が実行されるのではなく、キャッシュされた b.js の実行結果、つまり 4 行目が出力されます。

エクスポート.done = true;

つまり、CommonJS 入力は出力値のコピーであり、参照ではありません。

さらに、CommonJS モジュールは循環ロードに遭遇すると、すべてのコードが実行された後の値ではなく、現在実行されている部分の値を返すため、この 2 つには違いがある可能性があります。したがって、変数を入力するときは十分に注意する必要があります。

var a = require('a') // 安全な書き方
var foo = require('a').foo; // 危険な書き方

exports.good = 関数 (引数) {
  return a.foo('good', arg); // a.foo の最新の値が使用されます。
};

exports.bad = 関数 (引数) {
  return foo('bad', arg); // 部分的にロードされた値を使用します。
};

上記のコードでは、循環ロードが発生した場合、後で require('a').foo の値が上書きされる可能性があります。代わりに require('a') を使用する方が安全です。

ES6 モジュールの循環ロード

ES6 は CommonJS とは根本的に異なる「ループ読み込み」を処理します。 ES6 モジュールは動的参照です。「import」を使用してモジュールから変数をロードする場合 (つまり、「import foo from 'foo'」)、それらの変数はキャッシュされませんが、開発者が必要とするロードされたモジュールへの参照になります。 to 実際に値を取得する際には値が取得できることが保証されます。

以下の例を見てください。

// a.mjs
'./b' から {bar} をインポートします。
console.log('a.mjs');
コンソール.ログ(バー);
エクスポート let foo = 'foo';

// b.mjs
'./a' から {foo} をインポートします。
console.log('b.mjs');
コンソール.log(foo);
エクスポート let bar = 'bar';

上記のコードでは、「a.mjs」は「b.mjs」をロードし、「b.mjs」は「a.mjs」をロードして、循環ロードを形成します。 a.mjsを実行すると以下のようになります。

$node --experimental-modules a.mjs
b.mjs
参照エラー: foo が定義されていません

上記のコードでは、「a.mjs」の実行後にエラーが報告され、「foo」変数が定義されていません。これはなぜでしょうか。

ES6 ループの読み込みが行ごとにどのように処理されるかを見てみましょう。まず、「a.mjs」を実行した後、エンジンは「b.mjs」がロードされたことを検出するため、最初に「b.mjs」を実行し、次に「a.mjs」を実行します。そして、b.mjsを実行すると、a.mjsからfooインターフェースを入力することがわかりますが、この時点ではa.mjsは実行されませんが、このインターフェースはすでに存在していると考えられます。存在するため、以下の実行を続行します。 3行目の「console.log(foo)」を実行すると、このインターフェースが全く定義されていないことが分かり、エラーが報告されました。

この問題を解決する方法は、b.mjs の実行時に foo を定義させることです。これは関数として foo を書くことで解決できます。

// a.mjs
'./b' から {bar} をインポートします。
console.log('a.mjs');
console.log(bar());
関数 foo() { 'foo' を返す }
{foo} をエクスポートします。

// b.mjs
'./a' から {foo} をインポートします。
console.log('b.mjs');
console.log(foo());
関数 bar() { return 'bar' }
{バー} をエクスポートします。

この時点で「a.mjs」を実行すると期待通りの結果が得られます。

$node --experimental-modules a.mjs
b.mjs
ふー
a.mjs
バー

これは、この関数にはリフティング効果があるためです。「import {bar} from './b'」を実行すると、関数「foo」がすでに定義されているため、「b.mjs」のロード時にエラーが報告されません。これは、関数 foo が関数式として書き換えられた場合、エラーが報告されることも意味します。

// a.mjs
'./b' から {bar} をインポートします。
console.log('a.mjs');
console.log(bar());
const foo = () => 'foo';
{foo} をエクスポートします。

上記コードの4行目を関数式に変更するとプロモーション効果がなく、実行時にエラーが報告されます。

ES6 モジュール ローダー SystemJS によって提供される例を見てみましょう。

//偶数.js
'./odd' から { 奇数 } をインポートします
エクスポート変数カウンター = 0;
エクスポート関数 Even(n) {
  カウンタ++;
  n === 0 || 奇数(n - 1)を返します。
}

// 奇数.js
import { 偶数 } から './even';
エクスポート関数 od(n) {
  return n !== 0 && Even(n - 1);
}

上記のコードでは、even.js の関数 even にパラメータ n があり、それが 0 に等しくない限り、1 が減算され、ロードされた odd() が渡されます。 「odd.js」も同様の操作を行います。

上記のコードを実行すると、結果は次のようになります。

$babel-node
> import * as m from './even.js';
> m.even(10);
真実
> m.カウンター
6
> m.even(20)
真実
> m.カウンター
17

上記のコードでは、パラメータ n が 10 から 0 に変化すると、even() が合計 6 回実行されるため、変数 counter は 6 になります。 even() が 2 回目に呼び出されるとき、パラメータ n は 20 から 0 に変更されます。even() は前回の 6 回と合わせて合計 11 回実行されるため、変数 counter は次のようになります。 17に等しい。

この例を CommonJS に書き直すと、まったく実行されず、エラーが報告されます。

//偶数.js
var od = require('./odd');
var カウンタ = 0;
エクスポート.カウンター = カウンター;
エクスポート.even = 関数 (n) {
  カウンタ++;
  n == 0 || 奇数(n - 1)を返します。
}

// 奇数.js
var Even = require('./even').even;
module.exports = 関数 (n) {
  return n != 0 && Even(n - 1);
}

上記のコードでは、even.jsodd.js をロードし、さらに odd.jseven.js をロードして、「ループ読み込み」を形成します。このとき、実行エンジンはeven.jsの実行部分(結果はありません)を出力するため、odd.jsでは変数evenundefineに等しく、後ほど待つことになります。 even(n - 1) を呼び出すとエラーが報告されます。

$node
> var m = require('./even');
> m.even(10)
TypeError: Even は関数ではありません

作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0