2017年3月22日水曜日

requireの仕組み

普段node書くとき、何気なく使ってるrequireだけど、どんな風にモジュールが読み込まれてるのかコアコードの中を追ってみる。

https://github.com/joyent/node/blob/v0.11.14/lib/module.js#L362

Module.prototype.require = function(path) {
  assert(util.isString(path), 'path must be a string');
  assert(path, 'missing path');
  return Module._load(path, this);
};

こいつが各moduleが読み込まれた時にセットされるrequireの本体。

簡単なパラメータチェックをしてModule._loadに処理を渡している。

この時はまだpathresolveなどはまだしていない。 単純な移譲。

module._load

Module._load = function(request, parent, isMain) {
  // ...省略
 
  // [A]
  var filename = Module._resolveFilename(request, parent);
 
  // [B]
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }
 
  // [C]
  if (NativeModule.exists(filename)) {
    //...省略
    return NativeModule.require(filename);
  }
 
  // [D]
  var module = new Module(filename, parent);
  // ...省略
  Module._cache[filename] = module;
 
  // [E]
  var hadException = true;
  try {
 
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }
 
  return module.exports;
};

[A]

ここで初めてファイル名の解決が行われます。

Module._resolveFilenameというAPIを使ってパスを解決しています。

このAPIは第二引数のparent (requireを実行しているmodule自身)のパスから相対的に解決していきます(native moduleを除く)

[B]

ファイルのパスが先ほどの手順で解決されているのでこれをキーとして、すでにそのモジュールが読み込まれていてキャッシュが存在していればそれのexportsを返すというような実装になってます。

Module._cachefile名をキー、値にはmodule自身をつっこんでるキャッシュ用のオブジェクトです。

ここで注意したいのは返してるのはmodule.exportsの値だけです。

通常ロードされたモジュールに展開されるローカル変数moduleはその呼び出し元moduleparentプロパティ( http://nodejs.jp/nodejs.org_ja/api/modules.html#modules_module_parent)をもってるはずですが、この辺はrequireの度に設定されなおしたりはしません。

なのでmodule.parentをたよりにした実装してるとこの辺でいつか死ぬのでやめといたほうがいい。

以前死にました。( module.parent.filename is cached. · Issue #6149 · joyent/node · GitHub )

[C]

ロード対象にされてるモジュールがnativeなモジュール(pathとかfsとかそういうやつ)かどうかを判定し、そうであればnativeモジュール用のローダーで読み込みます。 (https://github.com/joyent/node/blob/v0.11.14/src/node.js#L783)

[D]

ここで新たなモジュールとしてModuleインスタンスを作ります。(ここで渡されてるparentmodule.parentとして永久に保持される)

そしてそれをそのままパスをキーとしてキャッシュします。

この段階では別にパスからソースをコンパイルしたりはしてません。

とくにコンストラクタ内にそういう処理はありません。

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if (parent && parent.children) {
    parent.children.push(this);
  }
 
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

[E]

ここでModule.loadを利用してソースをコンパイルし、module.exportsを返却してます。

この時読み込みに失敗したエラーはcatchはされないものの、その後のfinalycacheだけは綺麗に消されるので、ロード失敗時に変なキャッシュが残ることはないはずです。

次はソースをコンパイルするところを追います。

module.load

Module.prototype.load = function(filename) {
  // 省略
 
  // [A]
  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
 
  // [B]
  Module._extensions[extension](this, filename);
  this.loaded = true;
};

[A]

拡張子ごとのローダーを利用するため、どのローダーを利用するかの判別のためにファイルパスから拡張子を抜き出します。 (デフォルト .js)

ローダーがないような拡張子の場合はとりあえず.js用のローダーで試すみたいです。

[B]

拡張子ごとのローダーによって読み込みを開始します。

Module.exteisons[extension]というのがローダーです。

通常我々が利用するのは.js.jsonくらいでしょうか。 あと一応.nodeというローダーもあるみたいです。

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};
 
 
// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};

.jsonのほうは簡単ですね。

ファイルを同期的に読み出してstripBOMutf-8BOM(http://www.wdic.org/w/WDIC/UTF-8#BOM)を取り除いたものをJSON.parseしてオブジェクトにもどしてそれをmodule.exportsにセットしています。

.jsonはこれでおしまいです。

.jsのほうはもうちょっと複雑なのでBOMを排除したファイルコンテンツを取得したあと、module._compileに処理を移譲しています。

よくこのローダーはテストとかでrequireしたものをスタブに置き換えるために使われたりします。

javascript - How to stub require() / expect calls to the "root" function of a module? - Stack Overflow

ドキュメント上では廃止予定となってるけどコアコードに根深く存在してるので事実上これは廃止できませんみたいなこと書いてあるので複雑な感じですね。

でも、そういうことしたいとき、多分ほかの方法はvm使ったりとかもっと荒々しい方法とかになると思います。

module._compile

https://github.com/joyent/node/blob/v0.11.14/lib/module.js#L378

長かったけどこれで最後です。

Module.prototype._compile = function(content, filename) {
  var self = this;
 
  // [A]
  // remove shebang
  content = content.replace(/^\#\!.*/, '');
 
  // [B]
  function require(path) {
    return self.require(path);
  }
 
  require.resolve = function(request) {
    return Module._resolveFilename(request, self);
  };
 
  Object.defineProperty(require, 'paths', { get: function() {
    throw new Error('require.paths is removed. Use ' +
                    'node_modules folders, or the NODE_PATH ' +
                    'environment variable instead.');
  }});
 
  require.main = process.mainModule;
 
  // Enable support to add extra extension types
  require.extensions = Module._extensions;
  require.registerExtension = function() {
    throw new Error('require.registerExtension() removed. Use ' +
                    'require.extensions instead.');
  };
 
  require.cache = Module._cache;
 
  // [C]
  var dirname = path.dirname(filename);
 
  // 省略
 
  // create wrapper function
  // [D]
  var wrapper = Module.wrap(content);
  var compiledWrapper = runInThisContext(wrapper, { filename: filename });
 
  // 省略
 
  // [E]
  var args = [self.exports, require, self, filename, dirname];
  return compiledWrapper.apply(self.exports, args);
};

[A]

ソースからシェバン( #!/usr/bin/env node ←こういうの)があれば取り除きます

[B]

読み込まれるモジュールのローカル変数として使うrequireを定義します。

なかみは単純にmodule.requireです。

ここまででみてきたrequireと同じものです(属するmoduleは呼び元と呼び先とで違うけど)

requireのもつメソッド( http://nodejs.jp/nodejs.org_ja/api/globals#globals_require )を定義していきます。

require.resolveは中でModule._resolveFilenameに処理を移譲してますね。

これはmodule._loadの中でファイルパスを解決したメソッドです。

なのでrequire.resolveを使ってファイルパスを解決すればそのままモジュールのキャッシュキーが安全につくれたりします

あとはちらほら廃止になったapi用の対応がみられますね。

[C]

この読み込まれるモジュールのディレクトリパスを取得しています。

これが読み込まれるモジュールのローカル変数として使う__dirnameになります。

ちなみに__filenameはさきほどModule._resolveFilenameによって解決されたパスをそのままつかいます。

[D]

ソースをラップします。

以前この部分だけ記事にしました( Nodeのファイルスコープ - ぶれすとつーる )

実行コード(文字列)

'(function (exports, require, module, __filename, __dirname) { ' + source + '\n});'

こんな感じでラップするものです。

こうすることで読み込まれるモジュールにスコープができ、あらかじめそこに存在するローカル変数を(引数にセットすることで)用意することができます。

そしてこうしてできたコード文字列をrunInThisContextコンパイルします。

runInThisContextvmモジュールのvm.runInThisContextと同じです。

var runInThisContext = require('vm').runInThisContext;

これは実行元のローカル変数とかにはアクセスできないけど同じglobalを共有形式のコード評価です。

第二引数で渡してるfilenameのオプションはスタックトレース時の表示用の情報です。

これで function (exports, require, module, __filename, __dirname { [source] } が得られました。

[E]

あとはそれぞれ引数( self.exports, require, self, filename, dirname )applyでセットして実行しています。

selfは自身のmoduleをさします。

module.exportsexportsが同じ参照のものだということがここからわかりますね。

しばしばモジュール内で

exports = function () { ... }

が期待した動きをしないけどなんで??みたいな質問がwebに溢れてますがこれをみれば一目瞭然ですね。

ただのローカル変数なんだから参照を切るような代入をしたらmodule.exportsに反映されなくなりますね、exportsにはなんのマジック的要素もありません。

module.exports = function () { ... }

を使いましょう。

長くなりましたが各モジュールのrequireの動きをおってみました。

途中横道にそれそうな処理は省略しましたがnative_moduleの解釈の仕方などもあるので見ると収穫があるかもしれません。

 

0 件のコメント:

コメントを投稿