node模塊化的源碼解析

引言

首先說一下CommonJS 模塊和ES6模塊二者的區別,這裏就直接先直接給出二者的差異。

  • CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
  • CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。
  • ES6 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當前模塊

commonJS模塊化的源碼解析

首先是nodejs的模塊封裝器

(function(exports, require, module, __filename, __dirname) {
// 模塊的代碼實際上在這裏
});

以下是node的Module的源碼

先看一下我們require一個文件會做什麼

Module.prototype.require = function(id) {
  validateString(id, 'id');
  requireDepth++;
  try {
    return Module._load(id, this, /* isMain */ false);
  } finally {
    requireDepth--;
  }
};

走到這裏至少就佐證了CommonJS 模塊是運行時加載,因爲require實際就是module這個對象的一個方法,所以require一個js的模塊,必須是得在運行到某個module的require代碼時才能去加載另一個文件。

然後這裏指向了_load方法 這裏有個細節就是isMain這個的話其實就是node去區分加載模塊是否是主模塊的,因爲是require的所以必然不應該是一個主模塊,當時循環引用的場景除外。

Module.prototype._load

接下來就找到_load方法

Module._load = function(request, parent, isMain) {
  let relResolveCacheIdentifier;

  const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

  const mod = loadNativeModule(filename, request, experimentalModules);
  if (mod && mod.canBeRequiredByUsers) return mod.exports;

  // Don't call updateChildren(), Module constructor already does.
  const module = new Module(filename, parent);

  if (isMain) {
    process.mainModule = module;
    module.id = '.';
  }

  Module._cache[filename] = module;
  if (parent !== undefined) {
    relativeResolveCache[relResolveCacheIdentifier] = filename;
  }

  let threw = true;
  try {
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
      if (parent !== undefined) {
        delete relativeResolveCache[relResolveCacheIdentifier];
      }
    }
  }

  return module.exports;
};

首先看一下cache,它實際上是處理了多次require的情況,從源碼中可以發現多次require一個模塊,node永遠的使用了第一次的module對象,並未做更新)。cache的細節其實和node模塊輸出的變量爲什麼不能在運行時被改變也是有關係的。因爲就算運行中去改變某個模塊輸出的變量,然後在另一個地方再次require,可是此時module.exports由於有cache,所以並不會發生變化。但是這裏還不能說明CommonJS 模塊輸出的是一個值的拷貝。

接着來看new Module(filename, parent)實例化後運行的module.load
核心我們關注的代碼就是

Module._extensions[extension](this, filename);
這裏是加載的代碼,然後我們看一下js文件的加載

Module._extensions['.js'] = function(module, filename) {
    。。。
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(content, filename);
};

Module.prototype._compile

這裏就是我們編寫的js文件被加載的過程。
以下的代碼經過大量刪減

Module.prototype._compile = function(content, filename) {
  const compiledWrapper = wrapSafe(filename, content, this);
  const dirname = path.dirname(filename);
  const require = makeRequireFunction(this, redirects);
  var result;
  const exports = this.exports;
  const thisValue = exports;
  const module = this;
result = compiledWrapper.call(thisValue, exports, require, module,
                                  filename, dirname);

  return result;
};

首先
const require = makeRequireFunction(this, redirects);
這個是代碼中實際require關鍵詞是如何工作的關鍵。沒有什麼複雜的,主要是如何針對入參去找文件的,這裏就跳過了詳細的建議看一下node的官方文檔。這裏就是’CommonJS 模塊是運行時加載‘的鐵證,因爲require其實都依賴於node模塊的執行時的注入,內部require的module更加需要在運行時纔會被compile了。

另外注意到this.exports作爲參數傳遞到了wrapSafe中,而整個執行作用域鎖定在了this.exports這個對象上。這裏是’CommonJS 模塊的頂層this指向當前模塊‘這句話的來源。

再看一下核心的模塊形成的函數wrapSafe

function wrapSafe(filename, content, cjsModuleInstance) {
  ...
  let compiled;
  try {
    compiled = compileFunction(
      content,
      filename,
      0,
      0,
      undefined,
      false,
      undefined,
      [],
      [
        'exports',
        'require',
        'module',
        '__filename',
        '__dirname',
      ]
    );
  } catch (err) {
    ...
  }

  return compiled.function;
}

核心代碼可以說非常少,也就是一個閉包的構造器。也就是文章開頭提到的模塊封裝器。
compileFunction。這個接口可以看node對應的[api](http://nodejs.cn/api/vm.html#...
)。

再看一個node官方對require的整個的簡化版

function require(/* ... */) {
    //對應new Module中 this.export = {}
  const module = { exports: {} };
  
  //這裏的代碼就是對應了_load裏的module.load()
  ((module, exports) => {
    // Module code here. In this example, define a function.
    function someFunc() {}
    exports = someFunc;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = someFunc;
    // At this point, the module will now export someFunc, instead of the
    // default object.
  })(module, module.exports);
  
  //注意看_load最後的輸出
  return module.exports;
}

這個時候再比較一下_load的代碼是不是恍然大悟。

最後就是’CommonJS 模塊輸出的是一個值的拷貝‘的解釋了,在cache的機制中已經說明了爲什麼重複require永遠不會重複執行,而在上面的函數中可以看到我們使用的exports中的值的拷貝。
到這裏整個node的模塊化的特點也就都有很明確的解釋了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章