Nodejs_Require - zen0822/interview GitHub Wiki

Require 什么

目录

展开

执行 node 命令行

当我们在命令行中敲下:

node ./index.js

入口文件在 src/node_main.cc 中,主要任务为将参数传入 node::Start 函数 之后,src/node.cc 中的 node::LoadEnvironment 函数会被调用,初始化环境

// src/node_main.cc
// ...

int main(int argc, char *argv[]) {
  setvbuf(stderr, NULL, _IOLBF, 1024);
  return node::Start(argc, argv);
}
// src/node.cc
// ...
void LoadEnvironment(Environment* env) {
  CHECK(env->is_main_thread());
  // TODO(joyeecheung): Not all of the execution modes in
  // StartMainThreadExecution() make sense for embedders. Pick the
  // useful ones out, and allow embedders to customize the entry
  // point more directly without using _third_party_main.js
  USE(StartMainThreadExecution(env));
}

// ...
MaybeLocal<Value> StartMainThreadExecution(Environment* env) {
  // To allow people to extend Node in different ways, this hook allows
  // one to drop a file lib/_third_party_main.js into the build
  // directory which will be executed instead of Node's normal loading.
  if (NativeModuleEnv::Exists("_third_party_main")) {
    return StartExecution(env, "internal/main/run_third_party_main");
  }

  std::string first_argv;
  if (env->argv().size() > 1) {
    first_argv = env->argv()[1];
  }

  if (first_argv == "inspect" || first_argv == "debug") {
    return StartExecution(env, "internal/main/inspect");
  }

  if (per_process::cli_options->print_help) {
    return StartExecution(env, "internal/main/print_help");
  }

  if (per_process::cli_options->print_bash_completion) {
    return StartExecution(env, "internal/main/print_bash_completion");
  }

  if (env->options()->prof_process) {
    return StartExecution(env, "internal/main/prof_process");
  }

  // -e/--eval without -i/--interactive
  if (env->options()->has_eval_string && !env->options()->force_repl) {
    return StartExecution(env, "internal/main/eval_string");
  }

  if (env->options()->syntax_check_only) {
    return StartExecution(env, "internal/main/check_syntax");
  }

  if (!first_argv.empty() && first_argv != "-") {
    return StartExecution(env, "internal/main/run_main_module");
  }

  if (env->options()->force_repl || uv_guess_handle(STDIN_FILENO) == UV_TTY) {
    return StartExecution(env, "internal/main/repl");
  }

  return StartExecution(env, "internal/main/eval_stdin");
}

初始化环境

// src/node_main_instance.cc
// ...
int NodeMainInstance::Run() {
  // ...

  if (exit_code == 0) {
    {
      AsyncCallbackScope callback_scope(env.get());
      env->async_hooks()->push_async_ids(1, 0);
      LoadEnvironment(env.get());
      env->async_hooks()->pop_async_id(1);
    }

    // ...
  }

  // ...

  return exit_code;
}

StartMainThreadExecution 里面的判断条件 !first_argv.empty() && first_argv != "-" 成立运行的 js 文件

// lib/internal/main/run_main_module.js

'use strict';

const {
  prepareMainThreadExecution
} = require('internal/bootstrap/pre_execution');

prepareMainThreadExecution(true);

const CJSModule = require('internal/modules/cjs/loader');

markBootstrapComplete();

// Note: this actually tries to run the module as a ESM first if
// --experimental-modules is on.
// TODO(joyeecheung): can we move that logic to here? Note that this
// is an undocumented method available via `require('module').runMain`
CJSModule.runMain();

在该函数内则会接着调用 bootstrap_node.js src/node.js 中的代码,并执行 startup 函数:

// lib/internal/modules/cjs/loader.js
// ...

// Bootstrap main module.
Module.runMain = function() {
  // Load the main module--the command line argument.
  if (experimentalModules) {
    asyncESM.loaderPromise.then((loader) => {
      return loader.import(pathToFileURL(process.argv[1]).href);
    })
    .catch((e) => {
      internalBinding('errors').triggerUncaughtException(
        e,
        true /* fromPromise */
      );
    });
    return;
  }
  Module._load(process.argv[1], null, true);
};

所以,最后会执行到 Module._load(process.argv[1], null, true); 这条语句来加载模块,其实这个 Module._load 在 require 函数的代码中也会被调用:

// lib/module.js
// ...

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

所以说,当我们在命令行中敲下 node ./index.js,某种意义上,可以说随后 Node.js 的表现即为立刻进行一次 require , 即:

require('./index.js')

路径分析

随后的步骤就是 require 一个普通模块了,让我们继续往下看,Module._load 方法做的第一件事,便是调用内部方法 Module._resolveFilename ,而该内部方法在进行了一些参数预处理后,最终会调用 Module._findPath 方法,来得到需被导入模块的完整路径,让我们从代码中来总结出它的路径分析规则:

// lib/module.js
// ...

Module._findPath = function(request, paths, isMain) {
  const absoluteRequest = path.isAbsolute(request);
  if (absoluteRequest) {
    paths = [''];
  } else if (!paths || paths.length === 0) {
    return false;
  }

  const cacheKey = request + '\x00' +
                (paths.length === 1 ? paths[0] : paths.join('\x00'));
  const entry = Module._pathCache[cacheKey]; // 优先取缓存
  if (entry)
    return entry;

  var exts;
  var trailingSlash = request.length > 0 &&
    request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH;
  if (!trailingSlash) {
    trailingSlash = /(?:^|\/)\.?\.$/.test(request);
  }

  // For each path
  for (var i = 0; i < paths.length; i++) {
    // Don't search further if path doesn't exist
    const curPath = paths[i];
    if (curPath && stat(curPath) < 1) continue;
    var basePath = resolveExports(curPath, request, absoluteRequest);
    var filename;

    var rc = stat(basePath);
    if (!trailingSlash) {
      if (rc === 0) { // 若是文件.
        if (!isMain) {
          if (preserveSymlinks) {
            filename = path.resolve(basePath);
          } else {
            filename = toRealPath(basePath);
          }
        } else if (preserveSymlinksMain) {
          // For the main module, we use the preserveSymlinksMain flag instead
          // mainly for backward compatibility, as the preserveSymlinks flag
          // historically has not applied to the main module.  Most likely this
          // was intended to keep .bin/ binaries working, as following those
          // symlinks is usually required for the imports in the corresponding
          // files to resolve; that said, in some use cases following symlinks
          // causes bigger problems which is why the preserveSymlinksMain option
          // is needed.
          filename = path.resolve(basePath);
        } else {
          filename = toRealPath(basePath);
        }
      }

      if (!filename) {
        // 带上 .js .json .node 后缀进行尝试
        if (exts === undefined)
          exts = Object.keys(Module._extensions);
        filename = tryExtensions(basePath, exts, isMain);
      }
    }

    if (!filename && rc === 1) {  // 是目录.
      // 获取 package.json 中 main 属性的值
      if (exts === undefined)
        exts = Object.keys(Module._extensions);
      filename = tryPackage(basePath, exts, isMain, request);
    }

    if (filename) {
      Module._pathCache[cacheKey] = filename;
      return filename;
    }
  }
  return false;
};

总结分析规则

按顺序判断

如果 X 是内置模块(比如 require('http'))

  1. 返回该模块。
  2. 不再继续执行。

如果 X 以 "./" 或者 "/" 或者 "../" 开头

  1. 根据 X 所在的父模块,确定 X 的绝对路径。
  2. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
  • X
  • X.js
  • X.json
  • X.node
  1. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
  • X/package.json(main字段)
  • X/index.js
  • X/index.json
  • X/index.node

如果 X 不带路径

  1. 根据 X 所在的父模块,确定 X 可能的安装目录。
  2. 依次在每个目录中,将 X 当成文件名或目录名加载。

抛出错误 "not found"

例子

当前脚本文件 /home/zen/projects/foo.js 执行了 require('bar') ,这属于上面的第三种情况。Node 内部运行过程如下。

首先,确定 x 的绝对路径可能是下面这些位置,依次搜索每一个目录

  • /home/zen/projects/node_modules/bar
  • /home/zen/node_modules/bar
  • /home/node_modules/bar
  • /node_modules/bar

搜索时,Node 先将 bar 当成文件名,依次尝试加载下面这些文件,只要有一个成功就返回

  1. bar
  2. bar.js
  3. bar.json
  4. bar.node

如果都不成功,说明 bar 可能是目录名,于是依次尝试加载下面这些文件

  1. bar/package.json(main字段)
  2. bar/index.js
  3. bar/index.json
  4. bar/index.node

如果在所有目录中,都无法找到 bar 对应的文件或目录,就抛出一个错误

包装模块

在取得了模块的完整路径后,便该是执行模块了,我们以执行 .js 后缀的 JavaScript 模块为例。首先 Node.js 会通过 fs.readFileSync 方法,以 UTF-8 的格式,将 JavaScript 代码以字符串的形式读出,传递给内部方法 module._compile,在这个内部方法里,则会调用 Module.wrap 方法,将我们的模块代码包裹在一个函数中:

// lib/internal/modules/cjs/loader.js
// ...

let wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

const wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

let wrapperProxy = new Proxy(wrapper, {
  set(target, property, value, receiver) {
    patched = true;

    return Reflect.set(target, property, value, receiver);
  },

  defineProperty(target, property, descriptor) {
    patched = true;
    return Object.defineProperty(target, property, descriptor);
  }
});

Object.defineProperty(Module, 'wrap', {
  get() {
    return wrap;
  },

  set(value) {
    patched = true;
    wrap = value;
  }
});

Object.defineProperty(Module, 'wrapper', {
  get() {
    return wrapperProxy;
  },

  set(value) {
    patched = true;
    wrapperProxy = value;
  }
});

所以,这便解答了我们之前提出的,在 global 对象下取不到它们的问题,因为它们是以包裹在外的函数的参数的形式传递进来的。所以顺便提一句,我们平常在文件的顶上写的 use strict ,其实最终声明的并不是 script-level 的严格模式,而都是 function-level 的严格模式。

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象的方法相同。Reflect不是一个函数对象,因此它是不可构造的。 Reflect.set 方法允许你在对象上设置属性。它的作用是给属性赋值。 handler.defineProperty() 用于拦截对对象的 Object.defineProperty() 操作

最后一步, Node.js 会使用 vm.runInThisContext 执行这个拼接完毕的字符串,取得一个 JavaScript 函数,最后带着对应的对象参数执行它们,并将赋值在 module.exports 上的对象返回:

// lib/internal/modules/cjs/loader.js
// ...

// Run the file contents in the correct scope or sandbox. Expose
// the correct helper variables (require, module, exports) to
// the file.
// Returns exception, if any.
Module.prototype._compile = function(content, filename) {
  let compiledWrapper;
  const wrapper = Module.wrap(content);
  var result

  compiledWrapper = vm.runInThisContext(wrapper, {
    filename,
    lineOffset: 0,
    displayErrors: true,
    importModuleDynamically: experimentalModules ? async (specifier) => {
      const loader = await asyncESM.loaderPromise;
      return loader.import(specifier, normalizeReferrerURL(filename));
    } : undefined,
  });

  result = compiledWrapper.call(thisValue, exports, require, module,
  return result;
};

循环依赖

通过上文我们已经可以知道,在 Module._load 内部方法里 Node.js 在加载模块之前,首先就会把传模块内的 module 对象的引用给缓存起来(此时它的 exports 属性还是一个空对象),module.loaded 为 false,执行完模块了 module.loaded 设置为 true,然后执行模块内代码,在这个过程中渐渐为 module.exports 对象附上该有的属性。所以当 Node.js 这么做时,出现循环依赖的时候,仅仅只会让循环依赖点取到运行过程的值,而不会让 require 死循环卡住。一个经典的例子:

一加载 a.js 就会缓存就进入 Module._pathCache['a.js'],此时 a.js:module.exports = { },继续运行 exports.done = false,module.exports = { done: false },加载 b.js

// a.js
'use strict'
console.log('a starting')
exports.done = false

var b = require('./b')

console.log(`in a, b.done=${b.done}`)
exports.done = true
console.log('a done')

b.js 获得的 a 模块是去缓存指针地址里面取的,所以此时 a 模块的 module.export.done 是 false

// b.js
'use strict'
console.log('b start')
exports.done = false

let a = require('./a')

console.log(`in b, a.done=${a.done}`)
exports.done = true
console.log('b done')
// main.js
'use strict'
console.log('main start')

let a = require('./a')
let b = require('./b')

console.log(`in main, a.done=${a.done}, b.done=${b.done}`)

执行 node main.js ,打印:

main start
a starting
b start
in b, a.done=false => 循环依赖点取到了中间值
b done
in a, b.done=true
a done
in main, a.done=true, b.done=true

对比其他模块规范

cmd、umd 和 amd

ES Module

script Module

面试建议

CommonJS 规范和 ES Module 的区别:

commonJs是被加载的时候运行,esModule是编译的时候运行
commonJs输出的是值的浅拷贝,esModule输出值的引用
commentJs具有缓存。在第一次被加载时,会完整运行整个文件并输出一个对象,拷贝(浅拷贝)在内存中。下次加载文件时,直接从内存中取值

参考

⚠️ **GitHub.com Fallback** ⚠️