JavaScript のモジュール化と SeaJs ソース コード分析の詳細な紹介

黄舟
リリース: 2017-03-10 15:31:58
オリジナル
1284 人が閲覧しました

Web ページの構造はますます複雑になっています。以前と同じようにすべてのコードをファイルに入れると、次のような問題が発生します。相互に

  • JavaScriptファイルが大きくなり、読み込み速度に影響

  • 構造が複雑で保守が難しい

  • バックエンド(Javaなど)と比較すると、明らかな違いがわかります。 2009 年に、Ryan Dahl はサーバー プログラミングに JavaScript を使用するための Node.js プロジェクトを作成しました。これにより、「JS モジュラー プログラミング」が正式に誕生しました。

  • 基本原理

モジュールは関数の集合であり、大きなファイルをいくつかの小さなファイルに分割し、各ファイルに異なる関数を定義して、HTML に導入できます。

var module1 = new Object({
    _count : 0,
    m1 : function (){
        //...
    },
    m2 : function (){
        //...
    }
});
ログイン後にコピー

この欠点は次のとおりです。モジュールのメンバーが暴露されます!関数のローカル変数には外部からアクセスできないことがわかっているので、即時実行関数を使用して最適化できます。

var module1 = (function(){
    var _count = 0;
    var m1 = function(){
        //...
    };
    var m2 = function(){
        //...
    };
    return {
        m1 : m1, m2 : m2
    };
})();
ログイン後にコピー

誰もがさまざまな方法でモジュールを定義でき、それらがすべて特定の仕様に従うことができれば、次のような利点があります。素晴らしい: お互いに引用し合うことができます!

モジュール仕様

math.js モジュールは、node.js で次のように定義されています:

function add(a, b){
    return a + b;
}
exports.add = add;
ログイン後にコピー

他のモジュールで使用する場合は、グローバル require 関数を使用してロードするだけです:

var math = require('math');
math.add(2,3);
ログイン後にコピー

ただし、ブラウザはネットワーク環境ではこのようにプレイできないため、非同期 AMD 仕様があります:

require(['math'], function (math) {// require([module], callback);
    math.add(2, 3);
});
ログイン後にコピー

モジュールは次のように定義されます (モジュールは他のモジュールに依存できます):

define(function (){ // define([module], callback);
    var add = function (x,y){
        return x+y;
    };
    return { add: add };
});
ログイン後にコピー

他の多くのリソースをロードできますRequireJS を使用すると (ここを参照)、非常に優れており、非常に強力です。私は仕事で SeaJS を使用することが多く、使用されている仕様は CMD と呼ばれます。これは推奨されています (非同期モードを参照する必要があります):

可能な限り遅延!

依存モジュールの処理方法とAMDの違いは、

AMDは先行実行(依存フロントエンド)、CMDは遅延実行(依存が近くにある)です。

CMD でモジュールを定義する方法は次のとおりです:

define(function(require, exports, module) {
    var a = require('./a');
    a.doSomething();
    var b = require('./b');
    b.doSomething();
});
ログイン後にコピー

使用方法についてはドキュメントを直接参照してください。ここでは詳細は説明しません。

SeaJS のソースコード解析

初めてモジュール化に触れたとき、これは単純すぎると感じました:

script タグを作成するときに onload と src を設定するだけです!

実際はこんな感じですが、完全にそうとは限りません! SeaJS コード (sea-debug.js) を見てみましょう。モジュールは、読み込みプロセス中に次の状態を経る可能性があります:

var STATUS = Module.STATUS = {
    // 1 - The `module.uri` is being fetched
    FETCHING: 1,
    // 2 - The meta data has been saved to cachedMods
    SAVED: 2,
    // 3 - The `module.dependencies` are being loaded
    LOADING: 3,
    // 4 - The module are ready to execute
    LOADED: 4,
    // 5 - The module is being executed
    EXECUTING: 5,
    // 6 - The `module.exports` is available
    EXECUTED: 6,
    // 7 - 404
    ERROR: 7
}
ログイン後にコピー

Modul オブジェクトは、モジュール情報を維持するためにメモリ内で使用されます:
function Module(uri, deps) {
    this.uri = uri
    this.dependencies = deps || [] // 依赖模块ID列表
    this.deps = {} // 依赖模块Module对象列表
    this.status = 0 // 状态
    this._entry = [] // 在模块加载完成之后需要调用callback的模块
}
ログイン後にコピー

ページ上でモジュール システムを開始するには、次の操作を行う必要があります。 seajs .use メソッドを使用します:

seajs.use(‘./main’, function(main) {// 依赖及回调方法
    main.init();
});
ログイン後にコピー

読み込みプロセスの全体的なロジックは Module.prototype.load で確認できます: Modul对象来维护模块的信息:

Module.prototype.load = function() {
    var mod = this
    if (mod.status >= STATUS.LOADING) {
        return
    }
    mod.status = STATUS.LOADING
    var uris = mod.resolve() // 解析依赖模块的URL地址
    emit("load", uris)
    for (var i = 0, len = uris.length; i < len; i++) {
        mod.deps[mod.dependencies[i]] = Module.get(uris[i])// 从缓存取或创建
    }
    mod.pass(); // 将entry传递给依赖的但还没加载的模块
    if (mod._entry.length) {// 本模块加载完成
        mod.onload()
        return
    }
    var requestCache = {};
    var m;
    // 加载依赖的模块
    for (i = 0; i < len; i++) {
        m = cachedMods[uris[i]]
        if (m.status < STATUS.FETCHING) {
            m.fetch(requestCache)
        } else if (m.status === STATUS.SAVED) {
            m.load()
        }
    }
    for (var requestUri in requestCache) {
        if (requestCache.hasOwnProperty(requestUri)) {
            requestCache[requestUri]()
        }
    }
}
ログイン後にコピー

在页面上启动模块系统需要使用seajs.use方法:

Module.prototype.onload = function() {
    var mod = this
    mod.status = STATUS.LOADED
    for (var i = 0, len = (mod._entry || []).length; i < len; i++) {
        var entry = mod._entry[i]
        if (--entry.remain === 0) {
            entry.callback()
        }
    }
    delete mod._entry
}
ログイン後にコピー

加载过程的整体逻辑可以在Module.prototype.load中看到:

for (var i = 0, len = uris.length; i < len; i++) {
    exports[i] = cachedMods[uris[i]].exec();
}
if (callback) {
    callback.apply(global, exports)// 执行回调函数
}
ログイン後にコピー

总体上逻辑很顺就不讲了,唯一比较绕的就是_entry数组了。网上没有找到比较通俗易懂的文章,于是看着代码连蒙带猜地大概看懂了,其实只要记住它的目标即可:

当依赖的所有模块加载完成后执行回调函数!

换种说法:

数组_entry中保存了当前模块加载完成之后、哪些模块的依赖可能加载完成的列表(依赖的反向关系)!

举个例子,模块A依赖于模块B、C、D,那么经过pass之后的状态如下:

此时A中的remain为3,也就是说它还有三个依赖的模块没有加载完成!而如果模块B依赖模块E、F,那么在它load的时候会将A也传递出去:

有几个细节:

  1. 已经加载完成的模块不会被传播;

  2. 已经传播过一次的模块不会再次传播;

  3. 如果依赖的模块正在加载那么会递归传播;

维护好依赖关系之后就可以通过Module.prototype.fetch来加载模块,有两种sendRequest的实现方式:

  1. importScripts

  2. script

然后根据结果执行load或者error方法。依赖的所有模块都加载完成后就会执行onload方法:

var exports = isFunction(factory) ?
    factory.call(mod.exports = {}, require, mod.exports, mod) :
    factory
ログイン後にコピー
ログイン後にコピー

其中--entry.remain就相当于告诉entry对应的模块:你的依赖列表里面已经有一个完成了!而entry.remain === 0则说明它所依赖的所有的模块都已经加载完成了!那么此时将执行回调函数:

if (callback) {
    callback.apply(global, exports)
}
ログイン後にコピー
ログイン後にコピー

脚本下载完成之后会马上执行define方法来维护模块的信息:

没有显式地指定dependencies时会用parseDependencies来用正则匹配方法中的require()片段(指定依赖列表是个好习惯)。

接着执行factory

function require(id) {
    var m = mod.deps[id] || Module.get(require.resolve(id))
    if (m.status == STATUS.ERROR) {
        throw new Error(&#39;module was broken: &#39; + m.uri)
    }
    return m.exec()
}
ログイン後にコピー
ログイン後にコピー

全体のロジックは非常にスムーズです。少し複雑なのは _entry 配列だけです。ネット上にわかりやすい記事がなかったので、コードを見ながら推測しながらざっくりと理解しましたが、目的は 🎜🎜🎜 依存するモジュールが全て終わった後にコールバック関数を実行する、ということだけ覚えておいてください。ロードされました! 🎜🎜🎜言い換えると: 🎜🎜🎜 array_entry には、現在のモジュールがロードされた後にどのモジュールの依存関係がロードされる可能性があるか (依存関係の逆関係) のリストが保存されます。 🎜🎜🎜たとえば、モジュールAがモジュールB、C、Dに依存している場合、パス後のステータスは次のようになります: 🎜🎜🎜🎜この時点で、A の remain は 3 です。これは、A にまだ 3 つの依存モジュールが残っていることを意味します。ロードされました!そして、モジュール B がモジュール E と F に依存している場合、ロード時に A も渡されます: 🎜🎜🎜🎜いくつかの詳細があります: 🎜
    🎜🎜ロードされたモジュールは伝達されません; 🎜🎜🎜🎜は伝達されます一度渡されたモジュールは再度伝播されません 🎜🎜🎜🎜 依存モジュールがロードされている場合は、再帰的に伝播されます 🎜🎜
🎜 依存関係を維持した後、Module を渡すことができます。 .prototype.fetch</code >モジュールをロードするには、<code>sendRequest を実装する 2 つの方法があります: 🎜
    🎜🎜importScripts🎜🎜🎜🎜script🎜 🎜
🎜その後、結果に従って、load または error メソッドが実行されます。すべての依存モジュールがロードされた後、onload メソッドが実行されます: 🎜rrreee🎜 ここで、--entry.remain は、エントリに対応するモジュールに次のように指示することと同じです。依存関係リスト内にはすでに完成したものが 1 つあります。 entry.remain === 0 は、依存するすべてのモジュールがロードされていることを意味します。次に、この時点でコールバック関数が実行されます: 🎜rrreee🎜 スクリプトがダウンロードされた後、モジュール情報を維持するために define メソッドがすぐに実行されます: 🎜🎜🎜 依存関係が明示的に指定されていない場合、 parseDependency は、メソッド内の通常のパターンの require() フラグメントと一致するために使用されます (依存関係リストを指定することをお勧めします)。 🎜🎜🎜次に、factory メソッドを実行してモジュール データを生成します。 🎜
var exports = isFunction(factory) ?
    factory.call(mod.exports = {}, require, mod.exports, mod) :
    factory
ログイン後にコピー
ログイン後にコピー

然后执行你在seajs.use中定义的callback方法:

if (callback) {
    callback.apply(global, exports)
}
ログイン後にコピー
ログイン後にコピー

当你写的模块代码中require时,每次都会执行factory方法:

function require(id) {
    var m = mod.deps[id] || Module.get(require.resolve(id))
    if (m.status == STATUS.ERROR) {
        throw new Error(&#39;module was broken: &#39; + m.uri)
    }
    return m.exec()
}
ログイン後にコピー
ログイン後にコピー

到这里核心的逻辑基本上讲完了,补一张状态的转换图:

以后在用的时候就可以解释一些诡异的问题了!

总结

模块化非常好用,因此在ECMAScript 6中也开始支持,但是浏览器支持还是比较堪忧的~~

以上がJavaScript のモジュール化と SeaJs ソース コード分析の詳細な紹介の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!