乐趣区

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

退出移动版