最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • nodejs可以说的秘密之node index.js到底做了什么

    正文概述 掘金(Kelvin)   2021-02-15   1849

    前言 

    一直以来,都想好好系统的分析分析nodejs源码,既能帮助我们在运用nodejs编写代码时更加游刃有余,也算是对编写好代码方法的一种学习,所以接下来就想以nodejs源码为出发点,写一些关于nodejs周边的系列,会包括nodejs本身及包管理工具npm的原理,也会包含一些依赖nodejs而实现的前端热门工具,如webpackbabel等。

    平常工作太忙而疏于写文章总结沉淀,在2021新年之际,是该好好写一写了。接下来这篇文章会以node index.js这个我们非常熟悉的命令来揭开nodejs模块机制的实现原理。

    首先,众所周知,nodejs实现了CommonJS模块规范,而browser并没有,在nodejs内部存在一套完善的模块机制。作为和browser一样的js运行时宿主,这套模块机制极大的方便了外部js模块开发和使用。我们都知道nodejs的模块机制是通过exports导出内部变量或方法,然后在另外模块中通过require引入,那nodejs内部到底是怎么通过exports导出,又是怎么通过require来引入其他模块的呢?今天我们就来通过node index.js 这个例子来分析nodejs源码是怎么一步步实现模块机制的,话不多说,我们马上开始! 

    内部模块机制 

    首先,我们知道nodejs是通过c++写的,在build之后其内部已经内建了很多模块,包括我们熟悉的path路径解析fs文件系统http网络模块等。同时我们来看node源码的目录结构,src下都是核心模块的c++源码实现,在lib下会存在基于c++实现的对应核心模块的js实现,这些js 核心模块在build之后会被编译成二进制文件,在nodejs bootstrap之后,这些js核心模块就会被加载到内存,因此对于这些模块的引用是非常快的。 既然nodejs是通过c++写成的,那我们要搞明白node index.js这行命令的整个内部执行逻辑,我们就要直切重点,找到它的main方法,也就是在src/node_main.cc下。从下图可以看出main方法里主要是做了一些初始化工作,比如初始化所有的signal处理器及标准输入输出流,然后才进入到了真正的入口Node::Start, 

    nodejs可以说的秘密之node index.js到底做了什么

    也就是在node.cc下的Start方法,这Start方法里主要做了以下几件事情:

    创建NodeMainInstance实例

    • NodeMainInstance构造器中,创建V8运行时Isolate实例并注册到对应platform上,同时设置Microtasks策略
    • 并将基础环境信息env_info传入Run方法,这里的env_info里主要包括native_modules标识符列表等配置信息。

    nodejs可以说的秘密之node index.js到底做了什么执行NodeMainInstance::Run

    • 锁定Isolate实例,只用于当前线程,同时设置不可修改
    • 创建Isolate执行主作用域,类似于browser的全局作用域
    • 创建主运行时环境CreateMainEnvironment,初始化运行时执行上下文InitializeContextRuntime,同时设置诸如原型链原型变量名称__proto__等js语法层面的相关名称

    nodejs可以说的秘密之node index.js到底做了什么加载执行环境LoadEnvironment

    • 初始化libnv,初始化诊断工具
    • 根据process.argv[1]来选择特定的StartExecution,以node index.is为例,此时process.argv[1]为"index.js",则会进到StartExecution(env, "internal/main/run_main_module"),即通过加载内部核心js模块internal/main/run_main_module.js来执行具体的处理逻辑,通过这个内部核心js模块的名字我们也可以猜到这个模块是做什么用的,run_main_module,没错,就是nodejs执行外部模块的入口所在,因为nodejs在执行外部模块的时候会实例化一个MainModule。

    nodejs可以说的秘密之node index.js到底做了什么

    nodejs可以说的秘密之node index.js到底做了什么

    执行ExecuteBootstrapper,开始真正的执行逻辑

    • 通过NativeModuleLoader查找并编译LookAndCompile。上面说过,内部核心js模块在nodejs源码被build之后已经被编译成二进制文件,这里会给internal/main/run_main_module.js文件标识加上node:字符串来告知NativeModuleLoader内部模块加载器当前正要查找的模块是一个内部js模块,然后会直接从内存中加载对应的binary二进制文件内容。然后依赖V8运行时Isolate实例对其进行组装编译,整个过程如上面图中红框里的调用栈过程所示,核心步骤GetWrappedFunction中,结合获取到的internal/main/run_main_module模块的ScriptDetail脚本数据,再配合注入以下一系列占位符字符串:

    • process变量名

    • require方法名,此处的reqire方法更应该叫做native_module_require,是有别于我们自定义js模块里拿到的require方法,这个native_module_require事实上是nodejs内部模块引用的基础实现,而我们自己写的自定义js模块里的require事实上是在native_module_require上做了一层封装,后面会着重介绍,先按下不表 

    • internal_binding方法名,该方法是用于引用nodejs内部基于c++实现的核心模块的加载器方法,当我们编写自定义核心模块来对nodejs进行扩展时,外部模块就可以通过这个方法引用新添加的核心js模块,在内建的js核心模块中这个方法的名称是getInternalBinding 

    • primordials变量名,这个变量是nodejs内部对原生js众多类型构造器/方法的一个外观,防止当原生js构造器/方法被覆盖时导致出错 通过以上这些占位符的注入,也使得我们能在核心js模块内部使用这些变量。 

    nodejs可以说的秘密之node index.js到底做了什么

    最后通过NewFunctionFromSharedFunctionInfo对组装好的Script脚本生成NewFunction,最终得到待执行fn,如下图,传入以下变量执行这个函数,这些变量是分别对应编译时所传入的占位符

    • 执行上下文context,这个context即是V8运行时Isolate实例所绑定的执行上下文,后续内部模块的执行是在这个上下文内执行的
    • process对象,即是我们在外部模块能拿到的全局变量process,这样内部核心模块中就能使用process
    • native_module_require方法,用于引用其他内部核心js模块
    • internal_binding_loader方法,用于应用其他内部核心c++模块
    • primodials对象

    nodejs可以说的秘密之node index.js到底做了什么

    最后在当前主作用域下执行这个fn,至此nodejs的NodeMainInstance的c++执行部分已经完成,接下来就是核心模块internal/main/run_main_module该上场的时候了。

    外部模块执行 

    无论是c还是java,乃至我们的js,当我们的程序代码被编译成可执行文件最终加载到内存,最终作为指令集列表被加载进cpu寄存器执行,都是会有一个主函数入口。所以对于nodejs来说,它同样会在模块机制上存在一个最外层的主模块执行入口,所以从代码组织上我们也可以很快发现主入口逻辑所在,也就是下面要说的这个run_main_module,让我们来看下internal/main/run_main_module中做了什么。

    nodejs可以说的秘密之node index.js到底做了什么

    我们会发现run_main_module中最终用来执行外部模块的入口在internal/modules/cjs/loader模块下的Module.runMain(process.argv[1]),这里的process.argv[1]自然就是"index.js",而在run_main_module中还有另一个内部核心js模块internal/bootstrap/pre_execution的身影,在这个模块内部为Module挂载了runMain方法,即internal/modules/run_main的executeUserEntryPoint

    nodejs可以说的秘密之node index.js到底做了什么

    另外插一句,大家会发现这里也是用到了require来加载模块,正如上面所说的这里的require并不是我们平常自定义js模块中的require方法,它实际上是nativeModuleRequire

    nodejs可以说的秘密之node index.js到底做了什么

    NativeModule.map上缓存了所有的内部js核心模块,也就是一个个NativeModule实例,这里不贴代码大家可以移步自行前往源码查看,当我们require一个这样的模块时,实际上就会通过每个NativeModule实例mod.compileForInternalLoader进行加载,解读这个方法内部核心逻辑,我们可以发现nodejs模块机制的另一个神奇之处。当加载内部核心js模块时,nodejs会通过模块标识id进行compileFunction编译得到一个function,然后传入module.exportsrequiremoduleprocessinternalBindingprimordials这些变量,也就是说除internal/main/run_main_module这个比较特殊的内部模块之外,其他内部模块获得process、internalBinding和primordials这些变量的方式是通过nativeModuleRequire来实现的,这也解释了其他内部模块内部为什么能使用这些变量。

    nodejs可以说的秘密之node index.js到底做了什么

    OK,现在再回过头来看executeUserEntryPoint,从方法名字上想必大家也能马上看出来它的作用,它的核心作用就是通过Module._load来加载外部模块。因为对于nodejs来说,我们最经常使用就是CommonJS的模块规模,所以这里并不会利用useESMLoader,当然想使用esModule方式也很简单。

    nodejs可以说的秘密之node index.js到底做了什么

    那么现在问题的核心就在于弄清楚Module是一个什么,它是如何加载并执行外部模块的?我们直接前往Module所在的内部核心js模块/internal/modules/cjs/loader.js,我们直切重点Module._load。这里顺便提一下,关于nodejs模块机制的两个加速模块加载的方法:

    1. 针对内部核心js模块,会build成二进制文件,然后在nodejs bootstrap后直接加载到内存
    2. 针对于用户自定义外部js模块,会在每次加载后直接缓存到Module_.cache,同时也会对文件标识进行缓存,后续再次加载相同的文件时会直接从缓存中查找

    因此在Module._load中,首先会根据文件标识查找缓存,如果找不到,则解析当前文件标识Module._resolveFilename,根据Module._resolveLookupPaths逐级向上查找对应文件标识的模块,像上面的例子node index.js,会直接查找当前目录下的对应文件,找不到则报错,而如果是加载对应核心js模块,在默认下,会从当前目录下node_modules/下查找,找不到则逐级向上查找父级目录的node_modules/。这个过程也就是nodejs模块机制下的文件路劲解析。具体的逻辑大家可以前往源码部分看一下,并不复杂,这里就不在赘述了。找到文件后生成一个新的Module实例,最后进行该实例module.load。根据findLongestRegisteredExtension查找对应文件扩展名,默认会返回.js。接着就是核心的加载和编译过程了,根据对应的扩展名找到对应的加载器Module._extensions[extension],我们主要来看下.js文件的加载器实现。

    nodejs可以说的秘密之node index.js到底做了什么

    从上图源码中我们可以看到加载器中主要做两件事:

    1. 通过fs文件系统同步读取文件内容,因为是同步读取,所以这里更仰仗与nodejs模块机制里的缓存功能。
    2. 对文件内容进行module._compile编译执行。

    所有的魔法都是在module._compile中发生:

    我抓取了这个方法中最核心的源码,让我们一起来分析一下。首先就是进行对文件内容进行包裹组装wrapSafe,默认情况下,这个patched标志位是false,只有当wrapper被改变时会变成true,也就是说默认情况下,这里的组装是走的下面的compileFunction方式,在拿到compiledWrapper之后会注入module.exportsrequiremodulefilenamedirname,这也正是为什么我们的自定义模块中能拿到exports、require、module、__filename、__dirname这些变量,同时也是为什么module.exports和exports是同一个引用,而用exports=来导出会失败的原因。如果你仔细观察,会发现compiledWrapper调用的时候this指针指向的是module.exports,也就是说在自定义模块中我们是可以通过this.xx来导出变量/方法的。

    这里还要提一点,眼尖的同学肯定发现了,compiledWrapper传入的require其实是const require = makeRequireFunction(this, redirects),这里我就不贴makeRequireFunction方法的源码了,大家可以自行去看下源码,其实我上面也说了,我们自定义模块内拿到的require方法其实上是在nativeModuleRequire上的封装,也就是这个require方法同样支持引用内部核心js模块,这个能力取决于传入的redirects,否则会fallback到Module.protptype.require

    let wrap = function(script) { 
        return Module.wrapper[0] + script + Module.wrapper[1]; 
    }; 
    
    const wrapper = [ 
        '(function (exports, require, module, __filename, __dirname) { ', 
        '\n});' 
    ]; 
    
    function wrapSafe(filename, content, cjsModuleInstance) { 
        // 默认情况下,这个patched标志位是false,只有当wrapper被改变时会变成true 
        // 也就是说默认情况下,这里的组装是走的下面的 compileFunction 方式 
        if (patched) { 
            const wrapper = Module.wrap(content); 
            return vm.runInThisContext(wrapper, { 
                filename, 
                lineOffset: 0, 
                displayErrors: true, 
                importModuleDynamically: async (specifier) => { 
                    const loader = asyncESM.ESMLoader; 
                    return loader.import(specifier, normalizeReferrerURL(filename)); 
                }, 
            }); 
        } 
        let compiled; 
        try { 
            compiled = compileFunction( 
                content, 
                filename, 
                0, 
                0, 
                undefined, 
                false, 
                undefined, 
                [], 
                [ 'exports', // 这里会注入对应的变量名, 'require', 'module', '__filename', '__dirname', ] 
            ); 
        } catch (err) { 
            if (process.mainModule === cjsModuleInstance) enrichCJSError(err); 
            throw err; 
        } 
    
    Module.prototype._compile = function(content, filename) { 
        ... 
        const compiledWrapper = wrapSafe(filename, content, this); 
        ... 
        const dirname = path.dirname(filename); 
        const require = makeRequireFunction(this, redirects); 
        let result; 
        const exports = this.exports; 
        const thisValue = exports; // this指针指向了exports 
        const module = this; 
        ... 
        result = ReflectApply(compiledWrapper, thisValue, [exports, require, module, filename, dirname]); 
        ... 
        return result; 
    }; 
    

    以上就是我们关于nodejs模块机制的探索,看到这里,大家再回过头去看webpack这个目前前端业界使用比较广泛的构建工具,大家有没有一种webpack和nodejs同宗同源的感觉。其实大家可以借助这个思路来看webpack的源码,本质上,webpack就是把nodejs这套模块机制搬进了browser里。

    总结 

    至此我们已经分析完node index.js到底做了什么,其实里面还有一部内容值得我们深入研究下,就是nodejs如何通过V8我们的js进行编译,或者说V8是如何工作的,我认为搞清楚V8底层原理,对于我们前端程序员玩转js是很有帮助的,同时这部分内容我后续也会再起一个专题去专门研究,大家如果有什么干货想分享的,也非常欢迎多留言讨论,一起共同快速进步。


    起源地下载网 » nodejs可以说的秘密之node index.js到底做了什么

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元