1

引言

首先说一下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的模块化的特点也就都有很明确的解释了。


求实亭下
142 声望13 粉丝

有着深度学习技能的前端开发工程师


下一篇 »
webpack模块