node模塊化的源碼解析

引言

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

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

commonJS模塊化的源碼解析

首先是nodejs的模塊封裝器node

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

如下是node的Module的源碼git

先看一下咱們require一個文件會作什麼github

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代碼時才能去加載另外一個文件。api

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

Module.prototype._load

接下來就找到_load方法app

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文件的加載ui

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的模塊化的特色也就都有很明確的解釋了。

相關文章
相關標籤/搜索