1.先从Nodejs的入口谈起,以下采用的是nodejs 6.9.4 版本。

nodejs启动的入口文件src/node_main.cc

// UNIX
int main(int argc, char *argv[]) {
  // Disable stdio buffering, it interacts poorly with printf()
  // calls elsewhere in the program (e.g., any logging from V8.)
  setvbuf(stdout, nullptr, _IONBF, 0);
  setvbuf(stderr, nullptr, _IONBF, 0);
  return node::Start(argc, argv);
}

由代码可看出,主要运行的是node::Start方法,此方法定义在src/node.cc文件中。

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

  CHECK_GT(argc, 0);

  // Hack around with the argv pointer. Used for process.title = "blah".
  argv = uv_setup_args(argc, argv);

  // This needs to run *before* V8::Initialize().  The const_cast is not
  // optional, in case you're wondering.
  int exec_argc;
  const char** exec_argv;
  Init(&argc, const_cast<const char**>(argv), &exec_argc, &exec_argv);

#if HAVE_OPENSSL
#ifdef NODE_FIPS_MODE
  // In the case of FIPS builds we should make sure
  // the random source is properly initialized first.
  OPENSSL_init();
#endif  // NODE_FIPS_MODE
  // V8 on Windows doesn't have a good source of entropy. Seed it from
  // OpenSSL's pool.
  V8::SetEntropySource(crypto::EntropySource);
#endif

  v8_platform.Initialize(v8_thread_pool_size);
  V8::Initialize();

  int exit_code = 1;
  {
    NodeInstanceData instance_data(NodeInstanceType::MAIN,
                                   uv_default_loop(),
                                   argc,
                                   const_cast<const char**>(argv),
                                   exec_argc,
                                   exec_argv,
                                   use_debug_agent);
    StartNodeInstance(&instance_data);
    exit_code = instance_data.exit_code();
  }
  V8::Dispose();

  v8_platform.Dispose();

  delete[] exec_argv;
  exec_argv = nullptr;

  return exit_code;
}

此方法末尾处调用了同一个文件中的StartNodeInstance方法,传入一个NodeInstanceData(node_internals.h中定义)实例,在这个方法中调用了LoadEnvironment方法,LoadEnvironment方法有如下代码:

  // Execute the lib/internal/bootstrap_node.js file which was included as a
  // static C string in node_natives.h by node_js2c.
  // 'internal_bootstrap_node_native' is the string containing that source code.
  Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(),
                                                    "bootstrap_node.js");
  Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
  if (try_catch.HasCaught())  {
    ReportException(env, try_catch);
    exit(10);
  }
  // The bootstrap_node.js file returns a function 'f'
  CHECK(f_value->IsFunction());
  Local<Function> f = Local<Function>::Cast(f_value);

  // Add a reference to the global object
  Local<Object> global = env->context()->Global();

#if defined HAVE_DTRACE || defined HAVE_ETW
  InitDTrace(env, global);
#endif

#if defined HAVE_LTTNG
  InitLTTNG(env, global);
#endif

#if defined HAVE_PERFCTR
  InitPerfCounters(env, global);
#endif

  // Enable handling of uncaught exceptions
  // (FatalException(), break on uncaught exception in debugger)
  //
  // This is not strictly necessary since it's almost impossible
  // to attach the debugger fast enought to break on exception
  // thrown during process startup.
  try_catch.SetVerbose(true);

  env->SetMethod(env->process_object(), "_rawDebug", RawDebug);

  // Expose the global object as a property on itself
  // (Allows you to set stuff on `global` from anywhere in JavaScript.)
  global->Set(FIXED_ONE_BYTE_STRING(env->isolate(), "global"), global);

  // Now we call 'f' with the 'process' variable that we've built up with
  // all our bindings. Inside bootstrap_node.js and internal/process we'll
  // take care of assigning things to their places.

  // We start the process this way in order to be more modular. Developers
  // who do not like how bootstrap_node.js sets up the module system but do
  // like Node's I/O bindings may want to replace 'f' with their own function.
  Local<Value> arg = env->process_object();
  f->Call(Null(env->isolate()), 1, &arg);

可以看到,这个node通过执行/lib/internal/bootstrap_node.js进行初始化, ExecuteString之后得到的是bootstrap_node的一个对象,将其类型转换成Function,在最后执行之f->Call(Null(env->isolate()), 1, &arg);执行的时候传入的&arg就是process对象的引用。而process对象的设置是在相同文件的SetupProcessObject函数中设置的,在这个调用过程中在process对象上设置了除bind和dlopen外的几乎所有方法和对象。bootstrap_node.js执行初始化的就是Nodejs的module模块系统。

2.Node启动后会首先加载其核心模块(既纯用C++编写的Nodejs自带模块)。

而在js中引用这些模块都是使用process.binding函数进行引用,在process的初始化函数SetupProcessObject中可以看到如下代码:

 env->SetMethod(process, "binding", Binding);

也就是说实际使用的是node.cc文件中的Binding函数,如下:

  node_module* mod = get_builtin_module(*module_v);
  if (mod != nullptr) {
    exports = Object::New(env->isolate());
    // Internal bindings don't have a "module" object, only exports.
    CHECK_EQ(mod->nm_register_func, nullptr);
    CHECK_NE(mod->nm_context_register_func, nullptr);
    Local<Value> unused = Undefined(env->isolate());
    mod->nm_context_register_func(exports, unused,
      env->context(), mod->nm_priv);
    cache->Set(module, exports);
  } else if (!strcmp(*module_v, "constants")) {
    exports = Object::New(env->isolate());
    DefineConstants(env->isolate(), exports);
    cache->Set(module, exports);
  } else if (!strcmp(*module_v, "natives")) {
    exports = Object::New(env->isolate());
    DefineJavaScript(env, exports);
    cache->Set(module, exports);
  } else {
    char errmsg[1024];
    snprintf(errmsg,
             sizeof(errmsg),
             "No such module: %s",
             *module_v);
    return env->ThrowError(errmsg);
  }

  args.GetReturnValue().Set(exports);

可以看到,是调用get_builtin_module函数来获取编译好的c++模块的,看下get_builtin_module函数:

struct node_module* get_builtin_module(const char* name) {
  struct node_module* mp;

  for (mp = modlist_builtin; mp != nullptr; mp = mp->nm_link) {
    if (strcmp(mp->nm_modname, name) == 0)
      break;
  }

  CHECK(mp == nullptr || (mp->nm_flags & NM_F_BUILTIN) != 0);
  return (mp);
}

可以看到,这个函数在modlist_builtin结构中寻找对应输入名称的模块并返回。modlist_builtin结构是在node_module_register函数(在node.cc文件中被定义)调用时被逐条注册的,而node_module_register调用的地方是在node.h中的宏定义

#define NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, priv, flags)    \
  extern "C" {                                                        \
    static node::node_module _module =                                \
    {                                                                 \
      NODE_MODULE_VERSION,                                            \
      flags,                                                          \
      NULL,                                                           \
      __FILE__,                                                       \
      NULL,                                                           \
      (node::addon_context_register_func) (regfunc),                  \
      NODE_STRINGIFY(modname),                                        \
      priv,                                                           \
      NULL                                                            \
    };                                                                \
    NODE_C_CTOR(_register_ ## modname) {                              \
      node_module_register(&_module);                                 \
    }                                                                 \
  }
#define NODE_MODULE_CONTEXT_AWARE_BUILTIN(modname, regfunc)           \
  NODE_MODULE_CONTEXT_AWARE_X(modname, regfunc, NULL, NM_F_BUILTIN)   \

在每个src目录下的c++模块都会在结尾处调用NODE_MODULE_CONTEXT_AWARE_BUILTIN,也就是在模块被引入后就会将模块注册到modlist_builtin中去。供后续被其他模块引用。

3.接下来让我们看一下js环境初始化(bootstrap.js)中执行了什么。

从bootstrap.js文件整体是一个匿名函数,入参是process。此匿名函数执行后会调用其内部定义的startup函数。做的事情基本是先通过NativeModule对象的require方法引入各种Node的内置模块(lib目录下的js文件)进行初始化,之后通过process的第二个入参argv[1]即node进程启动时所带的参数来确定是主的js进程还是child进程。主进程通过run(Module.runMain);启动业务代码,子进程通过process的stdin读入code,再通过evalScript('[stdin]');去执行。
下面我们首先看下NativeMudule对象,

  function NativeModule(id) {
    this.filename = `${id}.js`;
    this.id = id;
    this.exports = {};
    this.loaded = false;
    this.loading = false;
  }

  NativeModule._source = process.binding('natives');
  NativeModule._cache = {};

  NativeModule.require = function(id) {
    if (id === 'native_module') {
      return NativeModule;
    }

    const cached = NativeModule.getCached(id);
    if (cached && (cached.loaded || cached.loading)) {
      return cached.exports;
    }

    if (!NativeModule.exists(id)) {
      throw new Error(`No such native module ${id}`);
    }

    process.moduleLoadList.push(`NativeModule ${id}`);

    const nativeModule = new NativeModule(id);

    nativeModule.cache();
    nativeModule.compile();

    return nativeModule.exports;
  };

可以看到,NativeModule是一个构造函数,其上绑定了一个静态方法require,一个新的模块被require后,会在process的moduleLoadList中注册之后实例化一个NativeModule实例,并调用该实例的cachecompile方法,最后返回NativeModule实例的exports属性。
先看cache函数:

  NativeModule.prototype.cache = function() {
    NativeModule._cache[this.id] = this;
  };

再看compile函数:

  NativeModule.prototype.compile = function() {
    var source = NativeModule.getSource(this.id);
    source = NativeModule.wrap(source);

    this.loading = true;

    try {
      const fn = runInThisContext(source, {
        filename: this.filename,
        lineOffset: 0,
        displayErrors: true
      });
      fn(this.exports, NativeModule.require, this, this.filename);

      this.loaded = true;
    } finally {
      this.loading = false;
    }
  };

compile函数先调用 NativeModule的静态方法getSource获取对应内置模块代码,再通过wrap进行包装,再使用runInThisContext编译返回一个可执行函数,最后执行之。

1.getSource方法如下:

  NativeModule.getSource = function(id) {
    return NativeModule._source[id];
  };

结合上面的代码可知,NativeModule的代码是通过process.binding('natives');获取的。
再回到Binding源码中,

  } else if (!strcmp(*module_v, "natives")) {
    exports = Object::New(env->isolate());
    DefineJavaScript(env, exports);
    cache->Set(module, exports);
  } else {

process.binding('natives');返回的是DefineJavaScript函数执行的结果,DefineJavaScript在node_javascript.cc中定义。

void DefineJavaScript(Environment* env, Local<Object> target) {
  HandleScope scope(env->isolate());

  for (auto native : natives) {
    if (native.source != internal_bootstrap_node_native) {
      Local<String> name = String::NewFromUtf8(env->isolate(), native.name);
      Local<String> source =
          String::NewFromUtf8(
              env->isolate(), reinterpret_cast<const char*>(native.source),
              NewStringType::kNormal, native.source_len).ToLocalChecked();
      target->Set(name, source);
    }
  }
}

可以看到,函数遍历的natives数组,也就是说natives数组中定一个了node内置模块的名称和代码。而natives定义的位置则比较特别,它是在编译时被动态写入到node_natives.h文件中的。这时候就需要看工程最外层的node.gyp文件了

...
'library_files': [
      'lib/internal/bootstrap_node.js',
      'lib/_debug_agent.js',
      'lib/_debugger.js',
      'lib/assert.js',
      'lib/buffer.js',
      'lib/child_process.js',
      'lib/console.js',
      'lib/constants.js',
      'lib/crypto.js',
...
]
...   
 {
      'target_name': 'node_js2c',
      'type': 'none',
      'toolsets': ['host'],
      'actions': [
        {
          'action_name': 'node_js2c',
          'inputs': [
            '<@(library_files)',
            './config.gypi',
          ],
          'outputs': [
            '<(SHARED_INTERMEDIATE_DIR)/node_natives.h',
          ],
          'conditions': [
            [ 'node_use_dtrace=="false" and node_use_etw=="false"', {
              'inputs': [ 'src/notrace_macros.py' ]
            }],
            ['node_use_lttng=="false"', {
              'inputs': [ 'src/nolttng_macros.py' ]
            }],
            [ 'node_use_perfctr=="false"', {
              'inputs': [ 'src/perfctr_macros.py' ]
            }]
          ],
          'action': [
            'python',
            'tools/js2c.py',
            '<@(_outputs)',
            '<@(_inputs)',
          ],
        },
      ],
    }, # end node_js2c

可以看到,这个配置项是启动python执行tools/js2c.py文件,输入是library_files即lib目录下的内置模块,输出是src/node_natives.h文件。具体js2c.py脚本不细述了,基本思路是按照c++头文件格式,将js内置模块写入到这样的一个结构中。

struct _native {
  const char* name;
  const unsigned char* source;
  size_t source_len;
};

这样js内置模块在node启动后就可被引用了。

3.对于文件模块的加载

1.对于文件模块和第三方模块,其他文件调用require时,即上文中boostrap.js中提到的require函数,首先会查看内存中是否已经加载了该模块,如果有就直接返回内存中的该模块。
所以模块在同一个node进程中只会加载一次,也就是说1.模块中的立即执行代码只会执行一次,2.因为在内存中是同一个对象,所以对该模块内状态的修改会影响到所有引入该模块的代码逻辑。

2.对于文件模块和第三方模块的加载工作,整个过程要进过”路径分析“、”模块定位“、”后缀分析“。
这几个过程会影响模块加载速度,如

a.”路径分析“:文件模块使用相对或绝对路径,寻址较快,第三方模块需要从当前调用文件的目录的node_module开始逐层向上查找,寻址较慢。
b."后缀分析",对无后缀的文件标识,引擎会自动补全js、json、node后缀;从js开始试起,所以没事儿还是加好后缀比较好,尤其node后缀。

3.获取到完整文件后,会以utf8格式同步读入文件,然后执行

var wrapper = Module.wrap(content);

var compiledWrapper = vm.runInThisContext(wrapper, {
  filename: filename,
  lineOffset: 0,
  displayErrors: true
});

// ...

var result = compiledWrapper.call(this.exports, this.exports, require, this,
  filename, dirname);

其中content就是读入的字节流,重点是wrap函数,这个函数是在字节流前后注入字符,使之变成如下形式:

'(function (exports, require, module, __filename, __dirname) { /* content */\n});'

其中exports、module、require这三个在文件中调用的对象就是在这里注入的。
综述:node的模块是对commonjs的具体实现,文件即模块,通过exports导出,require引入,模块可以根据逻辑按需加载,但加载过程是阻塞型的,即加载完成后才会继续执行后续逻辑代码
这部分可参考:https://zhuanlan.zhihu.com/p/376689147

        https://zhuanlan.zhihu.com/p/25916585

牛刀杀鸡
3 声望0 粉丝