首頁 > web前端 > js教程 > 探索 Node.js 原始碼,詳解cjs 模組的載入過程

探索 Node.js 原始碼,詳解cjs 模組的載入過程

青灯夜游
發布: 2022-09-07 20:13:53
轉載
2301 人瀏覽過

探索 Node.js 原始碼,詳解cjs 模組的載入過程

相信大家都知道如何在Node 中載入一個模組:

1

2

3

const fs = require('fs');

const express = require('express');

const anotherModule = require('./another-module');

登入後複製

沒錯,require就是載入cjs 模組的API,但V8 本身是沒有cjs 模組系統的,所以node 是怎麼透過require找到模組並且載入的呢? 【相關教學推薦:nodejs影片教學】      

我們今天將對 Node.js 原始碼進行探索,深入理解 cjs 模組的載入過程。 我們閱讀的node 程式碼版本為v17.x:

原始碼閱讀

內建模組為了知道

require

的工作邏輯,我們需要先了解內建模組是如何被載入到node 中的(諸如'fs','path','child_process',其中也包括一些無法被使用者引用的內部模組),準備好程式碼之後,我們首先要從node 啟動開始閱讀。 node 的main 函數在[src/node_main.cc](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node_main#Lccc/node_main#Lccc/node_main#Lcc/node_main#Lcc/node_main#Lcc/node_main#Lcc/node_main#Lcc/node_main#Lccc/node_main#Lccc/node_main#Lccc/node_main#Lcc/node_main#Lccc/node_main#Lcc/node_main#Lcc #[node::Start](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc#L1134)##no# 來啟動一個#rrde 實例:這裡建立一個#rrde 實例來啟動一個#rde 實例來啟動一個#了事件循環,並且創建了一個NodeMainInstance 的實例main_instance 並且呼叫了它的Run

方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

int Start(int argc, char** argv) {

  InitializationResult result = InitializeOncePerProcess(argc, argv);

  if (result.early_return) {

    return result.exit_code;

  }

 

  {

    Isolate::CreateParams params;

    const std::vector<size_t>* indices = nullptr;

    const EnvSerializeInfo* env_info = nullptr;

    bool use_node_snapshot =

        per_process::cli_options->per_isolate->node_snapshot;

    if (use_node_snapshot) {

      v8::StartupData* blob = NodeMainInstance::GetEmbeddedSnapshotBlob();

      if (blob != nullptr) {

        params.snapshot_blob = blob;

        indices = NodeMainInstance::GetIsolateDataIndices();

        env_info = NodeMainInstance::GetEnvSerializeInfo();

      }

    }

    uv_loop_configure(uv_default_loop(), UV_METRICS_IDLE_TIME);

 

    NodeMainInstance main_instance(&params,

                                   uv_default_loop(),

                                   per_process::v8_platform.Platform(),

                                   result.args,

                                   result.exec_args,

                                   indices);

    result.exit_code = main_instance.Run(env_info);

  }

 

  TearDownOncePerProcess();

  return result.exit_code;

}

登入後複製
Run 方法中呼叫[CreateMainEnvironment](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node_main_instance. ##建立Environment 物件env

並呼叫其[RunBootstrapping](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae296/nob. L398) 方法:<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">int NodeMainInstance::Run(const EnvSerializeInfo* env_info) {   Locker locker(isolate_);   Isolate::Scope isolate_scope(isolate_);   HandleScope handle_scope(isolate_);   int exit_code = 0;   DeleteFnPtr&lt;Environment, FreeEnvironment&gt; env =       CreateMainEnvironment(&amp;exit_code, env_info);   CHECK_NOT_NULL(env);   Context::Scope context_scope(env-&gt;context());   Run(&amp;exit_code, env.get());   return exit_code; }</pre><div class="contentsignin">登入後複製</div></div><div class="contentsignin">登入後複製</div></div>這裡的

[BootstrapInternalLoaders](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae22296b6d6實作了node 模組載入過程中非常重要的一步: 透過包裝並執行

[internal/bootstrap/loaders.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.模組的[nativeModulerequire](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders#jsL3332),用於載入的內建函數,332)用於載入的函數,332)。 [internalBinding](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.js#L##C 模組,用於載入的內建#C 模組,載入中的內建#. NativeModule](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.js#L191) 則是內建的小型模組來模組。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

Environment* CreateEnvironment(

    IsolateData* isolate_data,

    Local<Context> context,

    const std::vector<std::string>& args,

    const std::vector<std::string>& exec_args,

    EnvironmentFlags::Flags flags,

    ThreadId thread_id,

    std::unique_ptr<InspectorParentHandle> inspector_parent_handle) {

  Isolate* isolate = context->GetIsolate();

  HandleScope handle_scope(isolate);

  Context::Scope context_scope(context);

  // TODO(addaleax): This is a much better place for parsing per-Environment

  // options than the global parse call.

  Environment* env = new Environment(

      isolate_data, context, args, exec_args, nullptr, flags, thread_id);

#if HAVE_INSPECTOR

  if (inspector_parent_handle) {

    env->InitializeInspector(

        std::move(static_cast<InspectorParentHandleImpl*>(

            inspector_parent_handle.get())->impl));

  else {

    env->InitializeInspector({});

  }

#endif

 

  if (env->RunBootstrapping().IsEmpty()) {

    FreeEnvironment(env);

    return nullptr;

  }

 

  return env;

}

登入後複製
要注意的是,這個 require 函數只會被用於內建模組的加載,用戶模組的載入並不會用到它。 (這也是為什麼我們透過列印

require('module')._cache 可以看到所有使用者模組,卻看不到fs 等內建模組的原因,因為兩者的載入和快取維護方式並不一樣)。 使用者模組

接下來讓我們把目光移回

[NodeMainInstance::Run](https://github .com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node_main_instance.cc#L127) 函式:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

MaybeLocal<Value> Environment::RunBootstrapping() {

  EscapableHandleScope scope(isolate_);

 

  CHECK(!has_run_bootstrapping_code());

 

  if (BootstrapInternalLoaders().IsEmpty()) {

    return MaybeLocal<Value>();

  }

 

  Local<Value> result;

  if (!BootstrapNode().ToLocal(&result)) {

    return MaybeLocal<Value>();

  }

 

  // Make sure that no request or handle is created during bootstrap -

  // if necessary those should be done in pre-execution.

  // Usually, doing so would trigger the checks present in the ReqWrap and

  // HandleWrap classes, so this is only a consistency check.

  CHECK(req_wrap_queue()->IsEmpty());

  CHECK(handle_wrap_queue()->IsEmpty());

 

  DoneBootstrapping();

 

  return scope.Escape(result);

}

登入後複製
我們已經透過一個去#cronment 對象,這個Environment 實例已經有了一個模組系統NativeModule

用來維護內建模組。 然後程式碼會運行到Run 函數的另一個重載版本

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

function nativeModuleRequire(id) {

  if (id === loaderId) {

    return loaderExports;

  }

 

  const mod = NativeModule.map.get(id);

  // Can't load the internal errors module from here, have to use a raw error.

  // eslint-disable-next-line no-restricted-syntax

  if (!mod) throw new TypeError(`Missing internal module '${id}'`);

  return mod.compileForInternalLoader();

}

 

const loaderExports = {

  internalBinding,

  NativeModule,

  require: nativeModuleRequire

};

 

return loaderExports;

登入後複製

在這裡呼叫[LoadEnvironment](https://github.com /nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/api/environment.cc#L403)

<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">int NodeMainInstance::Run(const EnvSerializeInfo* env_info) {   Locker locker(isolate_);   Isolate::Scope isolate_scope(isolate_);   HandleScope handle_scope(isolate_);   int exit_code = 0;   DeleteFnPtr&lt;Environment, FreeEnvironment&gt; env =       CreateMainEnvironment(&amp;exit_code, env_info);   CHECK_NOT_NULL(env);   Context::Scope context_scope(env-&gt;context());   Run(&amp;exit_code, env.get());   return exit_code; }</pre><div class="contentsignin">登入後複製</div></div><div class="contentsignin">登入後複製</div></div>##nom]然後執行#/123de /blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc#L455)

<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">MaybeLocal&lt;Value&gt; StartExecution(Environment* env, StartExecutionCallback cb) {   // 已省略其他运行方式,我们只看 `node index.js` 这种情况,不影响我们理解模块系统   if (!first_argv.empty() &amp;&amp; first_argv != &quot;-&quot;) {     return StartExecution(env, &quot;internal/main/run_main_module&quot;);   } }</pre><div class="contentsignin">登入後複製</div></div> <p>在 <code>StartExecution(env, "internal/main/run_main_module")这个调用中,我们会包装一个 function,并传入刚刚从 loaders 中导出的 require 函数,并运行 [lib/internal/main/run_main_module.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/main/run_main_module.js) 内的代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

'use strict';

 

const {

  prepareMainThreadExecution

} = require('internal/bootstrap/pre_execution');

 

prepareMainThreadExecution(true);

 

markBootstrapComplete();

 

// Note: this loads the module through the ESM loader if the module is

// determined to be an ES module. This hangs from the CJS module loader

// because we currently allow monkey-patching of the module loaders

// in the preloaded scripts through require('module').

// runMain here might be monkey-patched by users in --require.

// XXX: the monkey-patchability here should probably be deprecated.

require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);

登入後複製

所谓的包装 function 并传入 require,伪代码如下:

1

2

3

(function(require/* 其他入参 */) {

  // 这里是 internal/main/run_main_module.js 的文件内容

})();

登入後複製

所以这里是通过内置模块require 函数加载了 [lib/internal/modules/cjs/loader.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/modules/cjs/loader.js#L172) 导出的 Module 对象上的 runMain 方法,不过我们在 loader.js 中并没有发现 runMain 函数,其实这个函数是在 [lib/internal/bootstrap/pre_execution.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/pre_execution.js#L428) 中被定义到 Module 对象上的:

1

2

3

4

5

6

7

8

9

function initializeCJSLoader() {

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

  if (!noGlobalSearchPaths) {

    CJSLoader.Module._initPaths();

  }

  // TODO(joyeecheung): deprecate this in favor of a proper hook?

  CJSLoader.Module.runMain =

    require('internal/modules/run_main').executeUserEntryPoint;

}

登入後複製

[lib/internal/modules/run_main.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/modules/run_main.js#L74) 中找到 executeUserEntryPoint 方法:

1

2

3

4

5

6

7

8

9

10

function executeUserEntryPoint(main = process.argv[1]) {

  const resolvedMain = resolveMainPath(main);

  const useESMLoader = shouldUseESMLoader(resolvedMain);

  if (useESMLoader) {

    runMainESM(resolvedMain || main);

  else {

    // Module._load is the monkey-patchable CJS module loader.

    Module._load(main, null, true);

  }

}

登入後複製

参数 main 即为我们传入的入口文件 index.js。可以看到,index.js 作为一个 cjs 模块应该被 Module._load 加载,那么 _load干了些什么呢?这个函数是 cjs 模块加载过程中最重要的一个函数,值得仔细阅读:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

// `_load` 函数检查请求文件的缓存

// 1. 如果模块已经存在,返回已缓存的 exports 对象

// 2. 如果模块是内置模块,通过调用 `NativeModule.prototype.compileForPublicLoader()`

//    获取内置模块的 exports 对象,compileForPublicLoader 函数是有白名单的,只能获取公开

//    内置模块的 exports。

// 3. 以上两者皆为否,创建新的 Module 对象并保存到缓存中,然后通过它加载文件并返回其 exports。

 

// request:请求的模块,比如 `fs`,`./another-module`,'@pipcook/core' 等

// parent:父模块,如在 `a.js` 中 `require('b.js')`,那么这里的 request 为 'b.js',

           parent 为 `a.js` 对应的 Module 对象

// isMain: 除入口文件为 `true` 外,其他模块都为 `false`

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

  let relResolveCacheIdentifier;

  if (parent) {

    debug('Module._load REQUEST %s parent: %s', request, parent.id);

    // relativeResolveCache 是模块路径缓存,

    // 用于加速父模块所在目录下的所有模块请求当前模块时

    // 可以直接查询到实际路径,而不需要通过 _resolveFilename 查找文件

    relResolveCacheIdentifier = `${parent.path}\x00${request}`;

    const filename = relativeResolveCache[relResolveCacheIdentifier];

    if (filename !== undefined) {

      const cachedModule = Module._cache[filename];

      if (cachedModule !== undefined) {

        updateChildren(parent, cachedModule, true);

        if (!cachedModule.loaded)

          return getExportsForCircularRequire(cachedModule);

        return cachedModule.exports;

      }

      delete relativeResolveCache[relResolveCacheIdentifier];

    }

  }

    // 尝试查找模块文件路径,找不到模块抛出异常

  const filename = Module._resolveFilename(request, parent, isMain);

  // 如果是内置模块,从 `NativeModule` 加载

  if (StringPrototypeStartsWith(filename, 'node:')) {

    // Slice 'node:' prefix

    const id = StringPrototypeSlice(filename, 5);

 

    const module = loadNativeModule(id, request);

    if (!module?.canBeRequiredByUsers) {

      throw new ERR_UNKNOWN_BUILTIN_MODULE(filename);

    }

 

    return module.exports;

  }

    // 如果缓存中已存在,将当前模块 push 到父模块的 children 字段

  const cachedModule = Module._cache[filename];

  if (cachedModule !== undefined) {

    updateChildren(parent, cachedModule, true);

    // 处理循环引用

    if (!cachedModule.loaded) {

      const parseCachedModule = cjsParseCache.get(cachedModule);

      if (!parseCachedModule || parseCachedModule.loaded)

        return getExportsForCircularRequire(cachedModule);

      parseCachedModule.loaded = true;

    else {

      return cachedModule.exports;

    }

  }

    // 尝试从内置模块加载

  const mod = loadNativeModule(filename, request);

  if (mod?.canBeRequiredByUsers) return mod.exports;

     

  // Don't call updateChildren(), Module constructor already does.

  const module = cachedModule || new Module(filename, parent);

 

  if (isMain) {

    process.mainModule = module;

    module.id = '.';

  }

    // 将 module 对象加入缓存

  Module._cache[filename] = module;

  if (parent !== undefined) {

    relativeResolveCache[relResolveCacheIdentifier] = filename;

  }

 

  // 尝试加载模块,如果加载失败则删除缓存中的 module 对象,

  // 同时删除父模块的 children 内的 module 对象。

  let threw = true;

  try {

    module.load(filename);

    threw = false;

  } finally {

    if (threw) {

      delete Module._cache[filename];

      if (parent !== undefined) {

        delete relativeResolveCache[relResolveCacheIdentifier];

        const children = parent?.children;

        if (ArrayIsArray(children)) {

          const index = ArrayPrototypeIndexOf(children, module);

          if (index !== -1) {

            ArrayPrototypeSplice(children, index, 1);

          }

        }

      }

    else if (module.exports &&

               !isProxy(module.exports) &&

               ObjectGetPrototypeOf(module.exports) ===

                 CircularRequirePrototypeWarningProxy) {

      ObjectSetPrototypeOf(module.exports, ObjectPrototype);

    }

  }

    // 返回 exports 对象

  return module.exports;

};

登入後複製

module 对象上的 [load](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/modules/cjs/loader.js#L963) 函数用于执行一个模块的加载:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

Module.prototype.load = function(filename) {

  debug('load %j for module %j', filename, this.id);

 

  assert(!this.loaded);

  this.filename = filename;

  this.paths = Module._nodeModulePaths(path.dirname(filename));

 

  const extension = findLongestRegisteredExtension(filename);

  // allow .mjs to be overridden

  if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs'])

    throw new ERR_REQUIRE_ESM(filename, true);

 

  Module._extensions[extension](this, filename);

  this.loaded = true;

 

  const esmLoader = asyncESM.esmLoader;

  // Create module entry at load time to snapshot exports correctly

  const exports = this.exports;

  // Preemptively cache

  if ((module?.module === undefined ||

       module.module.getStatus() < kEvaluated) &&

      !esmLoader.cjsCache.has(this))

    esmLoader.cjsCache.set(this, exports);

};

登入後複製

实际的加载动作是在 Module._extensions[extension](this, filename); 中进行的,根据扩展名的不同,会有不同的加载策略:

  • .js:调用 fs.readFileSync 读取文件内容,将文件内容包在 wrapper 中,需要注意的是,这里的 requireModule.prototype.require 而非内置模块的 require 方法。

1

2

3

4

const wrapper = [

  '(function (exports, require, module, __filename, __dirname) { ',

  '\n});',

];

登入後複製
  • .json:调用 fs.readFileSync 读取文件内容,并转换为对象。
  • .node:调用 dlopen 打开 node 扩展。

Module.prototype.require 函数也是调用了静态方法 Module._load实现模块加载的:

1

2

3

4

5

6

7

8

9

10

11

12

13

Module.prototype.require function(id) {

  validateString(id, 'id');

  if (id === '') {

    throw new ERR_INVALID_ARG_VALUE('id', id,

                                    'must be a non-empty string');

  }

  requireDepth++;

  try {

    return Module._load(id, this, /* isMain */ false);

  } finally {

    requireDepth--;

  }

};

登入後複製

总结

看到这里,cjs 模块的加载过程已经基本清晰了:

  • 初始化 node,加载 NativeModule,用于加载所有的内置的 js 和 c++ 模块

  • 运行内置模块 run_main

  • run_main 中引入用户模块系统 module

  • 通过 module_load 方法加载入口文件,在加载时通过传入 module.requiremodule.exports 等让入口文件可以正常 require 其他依赖模块并递归让整个依赖树被完整加载。

在清楚了 cjs 模块加载的完整流程之后,我们还可以顺着这条链路阅读其他代码,比如 global 变量的初始化,esModule 的管理方式等,更深入地理解 node 内的各种实现。

更多node相关知识,请访问:nodejs 教程

以上是探索 Node.js 原始碼,詳解cjs 模組的載入過程的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
最新問題
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板