本文只探讨 CommonJS 标准,不波及 ESM
咱们晓得 JavaScript 这门语言诞生之初次要是为了实现网页上表单的一些规定校验以及动画制作,所以布兰登. 艾奇(Brendan Eich)只花了一周多就把 JavaScript 设计进去了。能够说 JavaScript 从出世开始就带着许多缺点和毛病,这一点始终被其余语言的编程者所讥笑。随着 BS 开发模式慢慢地火了起来,JavaScript 所要承当的责任也越来越大,ECMA 接手标准化之后也慢慢的开始欠缺了起来。
在 ES 6 之前,JavaScript 始终是没有本人的模块化机制的,JavaScript 文件之间无奈互相援用,只能依赖脚本的加载程序以及全局变量来确定变量的传递程序和传递形式。而 script 标签太多会导致文件之间依赖关系凌乱,全局变量太多也会导致数据流相当错乱,命名抵触和内存透露也会更加频繁的呈现。直到 ES 6 之后,JavaScript 开始有了本人的模块化机制,不必再依赖 requirejs、seajs 等插件来实现模块化了。
在 Nodejs 呈现之前,服务端 JavaScript 基本上处于一片荒凉的境况,而过后也没有呈现 ES 6 的模块化标准(Nodejs 最早从 V8.5 开始反对 ESM 标准:Node V8.5 更新日志),所以 Nodejs 采纳了过后比拟先进的一种模块化标准来实现服务端 JavaScript 的模块化机制,它就是 CommonJS,有时也简称为 CJS。
这篇文章次要解说 CommonJS 在 Nodejs 中的实现。
在 Nodejs 采纳 CommonJS 标准之前,还存在以下毛病:
- 没有模块零碎
- 规范库很少
- 没有标准接口
- 不足包管理系统
这几点问题的存在导致 Nodejs 始终难以构建大型的我的项目,生态环境也是非常的贫乏,所以这些问题都是亟待解决的。
CommonJS 的提出,次要是为了补救以后 JavaScript 没有模块化规范的缺点,以达到像 Java、Python、Ruby 那样可能构建大型利用的阶段,而不是仅仅作为一门脚本语言。Nodejs 可能领有明天这样凋敝的生态系统,CommonJS 功不可没。
1.1 CommonJS 的模块化标准
CommonJS 对模块的定义非常简略,次要分为模块援用、模块定义和模块标识三个局部。上面进行简略介绍:
1.1.1、模块援用
示例如下:
在 CommonJS 标准中,存在一个 require“全局”办法,它承受一个标识,而后把标识对应的模块的 API 引入到以后模块作用域中。
1.1.2、模块定义
咱们曾经晓得了如何引入一个 Nodejs 模块,然而咱们应该如何定义一个 Nodejs 模块呢?在 Nodejs 上下文环境中提供了一个 module 对象和一个 exports 对象,module 代表以后模块,exports 是以后模块的一个属性,代表要导出的一些 API。在 Nodejs 中,一个文件就是一个模块,把办法或者变量作为属性挂载在 exports 对象上即可将其作为模块的一部分进行导出。
在另一个文件中,咱们就能够通过 引入之前定义的这个模块:
1.1.3、模块标识
模块标识就是传递给 函数的参数,在 Nodejs 中就是模块的 id。它必须是合乎小驼峰命名的字符串,或者是以.、.. 结尾的相对路径,或者绝对路径,能够不带后缀名。
模块的定义非常简略,接口也很简洁。它的意义在于将类聚的办法和变量等限定在公有的作用于域中,同时反对引入和导出性能以顺畅的连贯上下游依赖。
CommonJS 这套模块导出和引入的机制使得用户齐全不用思考变量净化。
以上只是对于 CommonJS 标准的简略介绍,更多具体的内容能够参考:CommonJS 标准
Nodejs 在实现中并没有齐全依照标准实现,而是对模块标准进行了肯定的取舍,同时也减少了一些本身须要的个性。接下来咱们会探索一下 Nodejs 是如何实现 CommonJS 标准的。
在 Nodejs 中引入模块会通过以下三个步骤:
- 路径分析
- 文件定位
- 编译执行
在理解具体的内容之前咱们先理解两个概念:
- 外围模块:Nodejs 提供的内置模块,比方 、、 等
- 文件模块:用户本人编写的模块,比方 、 等
外围模块在 Nodejs 源代码的编译过程中曾经编译进了二进制文件,Nodejs 启动时会被间接加载到内存中,所以在咱们引入这些模块的时候就省去了文件定位、编译执行这两个步骤,加载速度比文件模块要快很多。
文件模块是在运行的时候动静加载,须要走一套残缺的流程:、、 等,所以文件模块的加载速度比外围模块要慢。
2.1 优先从缓存加载
在解说具体的加载步骤之前,咱们该当通晓的一点是,Nodejs 对于曾经加载过一边的模块会进行缓存,模块的内容会被缓存到内存当中,如果下次加载了同一个模块的话,就会从内存中间接取出来,这样就省去了第二次路径分析、文件定位、加载执行的过程,大大提高了加载速度。无论是外围模块还是文件模块, 对同一文件的第二次加载都一律会采纳缓存优先的形式,这是第一优先级的。然而外围模块的缓存查看优先于文件模块的缓存查看。
咱们在 Nodejs 文件中所应用的 函数,实际上就是在 Nodejs 我的项目中的 lib/internal/modules/cjs/loader.js 所定义的 函数,只不过在前面的 函数中还会进行一层封装, 源码如下:
能够看到它最终应用了 办法来加载咱们的标识符所指定的模块,找到 :
咱们能够先简略的看一下源代码,其实代码正文曾经写得很分明了。
Nodejs 先会依据模块信息解析出文件门路和文件名,而后以文件名作为 对象的键查问该文件是否曾经被缓存,如果曾经被缓存的话,间接返回缓存对象的 属性。否则就会应用 从新解析文件名,再查问一边缓存对象。否则就会当做外围模块来加载,外围模块应用 办法进行加载。
如果通过了以上几个步骤之后,在缓存中依然找不到 require 加载的模块对象,那么就应用 构造方法从新结构一个新的模块对象。加载结束之后还会缓存到 对象中,以便下一次加载的时候能够间接从缓存中取到。
从源码来看,跟咱们之前说的没什么区别。
参考 nodejs 进阶视频解说:进入学习
2.2 路径分析
咱们晓得标识符是进行路径分析和文件定位的根据,在援用某个模块的时候咱们就会给 函数传入一个标识符,依据咱们应用的经验不难发现标识符基本上能够分为以下几种:
- 外围模块:比方 、 等
- 文件模块:这类模块的标识符是一个门路字符串,指向工程内的某个文件
- 非门路模式的文件模块:也叫做自定义模块,比方 、 等
标识符类型不同,加载的形式也有差别,接下来我将介绍不同标识符的加载形式。
2.2.1 外围模块
外围模块的加载优先级仅次于缓存,前文提到过因为外围模块的代码曾经编译成了二进制代码,在 Nodejs 启动的时候就会加载到内存中,所以外围模块的加载速度十分快。它基本不须要进行路径分析和文件定位,如果你想写一个和外围模块同名的模块的话,它是不会被加载的,因为其加载优先级不如外围模块。
2.2.2 门路模式的文件模块
当标识符为门路字符串时, 都会把它当做文件模块来加载,在依据标识符取得实在门路之后,Nodejs 会将实在门路作为键把模块缓存到一个对象里,使二次加载更快。
因为文件模块的标识符指明了模块文件的具体位置,所以加载速度相对而言也比拟快。
2.2.3 自定义模块
自定义模块是一个蕴含 的我的项目所结构的模块,它是一种非凡的模块,其查找形式比较复杂,所以耗时也是最长的。
在 Nodejs 中有一个叫做模块门路的概念,咱们新建一个 的文件,而后在其中输出如下内容:
而后应用 Nodejs 运行:
咱们能够看到控制台输出大抵如下:
此时我的 文件是放在桌面的,所以能够看到这个文件模块的模块门路是以后文件同级目录下的 ,如果找不到的话就从父级文件夹的同名目录下找,晓得找到根目录下。这种查找形式和 JavaScript 中的作用域链十分类似。能够看到当文件门路越深的时候查找所耗时间越长,所以这也是自定义模块加载速度最慢的起因。
在 Windows 环境中,Nodejs 通过上面函数获取模块门路:
代码和正文都写得很明确,大家看看就行,常量都放在 这个模块。
2.3 文件定位
2.3.1 文件扩展名剖析
咱们在援用模块的很多时候,传递的标识符都不会携带扩展名,比方
很显著上面的形式更简洁,然而 Nodejs 在定位文件的时候还是会帮咱们补齐。补齐的程序顺次为:、 和 ,在补齐的时候 Nodejs 会顺次进行尝试。在尝试的时候 Nodejs 会调用 模块来判断文件是否存在,所以这里可能会存在性能问题,如果在援用模块的时候加上扩展名,能够使得模块加载的速度变得更快。
在 Nodejs 源码 中,咱们能够看到当解析不到文件名的时候,会尝试应用 办法来增加扩展名:
而尝试的扩展名就是 的键值,检索代码不难发现代码中顺次定义了 、、、 等键,所以 函数会顺次进行尝试:
其中又调用了 办法:
能够看到最终还是依赖了 fs.realpathSync 办法,所以这里就跟之前说的是一样的,可能会存在性能问题,如果咱们间接带上了扩展名的话,间接就能够解析出 ,就不会去尝试扩展名了,这样能够略微进步一点加载速度。
2.3.2 目录和包剖析
咱们写的文件模块可能是一个 npm 包,此时包内蕴含许多 js 文件,所以 Nodejs 加载的时候又须要定位文件。Nodejs 会查找 文件,应用 来解析 json,随后取出其 字段之后对文件进行定位,如果文件名短少扩大的话,也会进入扩展名尝试环节。
如果 字段指定的文件名有误,或者压根没有 文件,那么 Nodejs 会将 当做默认文件名,随后开始尝试扩展名。
2.4 模块编译
Nodejs 中每一个模块就是一个 类实例, 的构造函数如下:
编译和执行是引入文件模块的最初一个环节,定位到具体文件后,Nodejs 会新建一个模块对象,而后依据门路载入缓存当前进行编译,扩展名不同,编译的形式也不同,它们的编译办法都注册在了 对象上,前文有提到过:
- 文件:通过同步读取文件内容后编译执行
- 文件:通过 模块读取文件,之后应用 转化成 JS 对象
- 文件:这是应用 C/C++ 编写的扩大模块,通过内置的 dlopen 办法加载最初编译生成的文件
- 文件:这是 Nodejs 反对 ESM 加载形式的模块文件,所以应用 办法载入的时候会间接抛出谬误
在 Nodejs 的 辅助函数模块 中,通过以下代码把 传递给了 函数:
所以咱们能够通过在模块中打印 查看以后 Nodejs 可能解析的模块:
另外咱们能够看到下面第二段代码中的正文:,也就是说咱们能够通过批改 对象来注册模块的解析办法。
比方咱们有一个 文件,咱们想把它解析成一个二维数组,那么咱们就能够写一下办法注册:
然而在 开始 Nodejs 就不再举荐应用这种形式来扩大加载形式了,而是冀望现将其余语言转化为 JavaScript 当前再加载执行,这样就防止了将简单的编译加载过程引入到 Nodejs 的执行过程。
接下来咱们理解一下 Nodejs 内置的几种模块的加载形式。
2.4.1 JavaScript 模块的编译
在咱们编写 Nodejs 模块的时候咱们能够随便的应用 、、、 和 等变量,好像它们都是 Nodejs 内置的全局变量一样,然而实际上他们都是局部变量。在 Nodejs 加载 JavaScript 模块的时候,会主动将模块内的所有代码包裹到一个匿名函数内,形成一个部分作用域,顺便把 ……等变量传入了匿名函数外部,所以咱们的代码能够随便应用这些变量。
假如咱们的模块代码如下:
通过 Nodejs 加载之后,代码变成了上面这样:
这样看起来的话,所有都变得很顺其自然了。这也是为什么每个模块都是独立的命名空间,在模块文件内轻易命名变量而不必放心全局变量净化,因为这些变量都定义在了函数外部,成为了这个包裹函数的公有变量。
弄明确 Nodejs 加载 JavaScript 的原理之后,咱们很容易就能够弄明确为什么不能给 间接赋值了,根本原因就在于 JavaScript 是一门按值传递(Pass-by-Value)的语言,不论咱们给变量赋值的是援用类型还是原始类型,咱们失去变量失去的都是一个值,只不过赋值援用类型时,变量失去的是一个代表存储援用类型的内存地址值(能够了解为指针),而咱们应用变量时 JavaScript 会依据这个值去内存中找到对应的援用类型值,所以看起来也像是援用传递。而一旦咱们给 这种变量从新赋值的时候, 就失去了对原来援用类型的指向,转而指向新的值,所以就会导致咱们赋给 的值并没有指向原来的援用类型对象。
看看上面这段代码:
能够看到函数内对 obj 从新赋值基本不影响函数内部的 对象,所以如果咱们在模块内(及包裹函数内)批改 的指向的话,内部的 对象基本不受影响,咱们导出的操作也就失败了。
上面咱们略微看一下 Nodejs 源码是如何编译执行 JavaScript 代码的。
首先依据 对象上注册的 模块加载办法找到入口:
能够看到加载办法听过 办法同步读取了 的文件内容之后,就把内容交给 的 办法去解决了,这个办法位于 类的原型上,咱们持续找到 办法:
能够看到最初还是交给了 办法来解决模块内容( 是做断电调试用的,咱们能够不论它),持续看 办法。
办法来源于 的执行后果:
而 函数的定义如下:
下面这段代码能够看到 办法通过 将模块代码结构成了一个匿名函数,随后扔给了 或者 去执行,这两函数都开始波及到 JavaScript 跟 C/C++ 的底层了,作者程度渣渣,不再进行下一步解读,感兴趣的童鞋能够本人找到源码持续浏览。
2.4.2 C/C++ 模块的编译
Nodejs 通过调用 process.dlopen 加载和执行 C/C++ 模块,该函数在 Window 和 *nix 零碎下有不同的实现,通过 linuv 兼容层进行了封装。
实际上 模块不须要编译,因为是依据 C/C++ 编译而成的,所以只有加载和执行过程。编写 C/C++ 模块可能进步 Nodejs 的扩大能力和计算能力,咱们晓得 Nodejs 是单线程异步无阻塞的语言,劣势在于 IO 密集型场景而非计算密集型场景。当咱们有大量的计算操作须要执行时,咱们能够将计算操作放到 C/C++ 模块中执行,这样能够晋升 Nodejs 在计算密集型场景下的体现。然而 C/C++ 的编程门槛比 Nodejs 高很多,所以这也是一大毛病。
Nodejs 在 中引入了 Worker Threads 个性,并且这一个性在 中开始默认启用,大大提高了 Nodejs 在计算密集型场景下的体现,在某种程度上缩小了开发者所须要编写的 C/C++ 代码量。
2.4.3 JSON 文件的编译
JSON 文件的编译是最简略的,通过 读取文件内容后,调用 转化成 JavaScript 对象导出就行了。
因为作者程度无限,对于外围模块以及 C/C++ 模块的书写和编译不再解说。
通过这篇文章,咱们至多学习到了以下几点:
- CommonJS 模块化标准的根本内容
CommonJS 标准次要包含 模块援用 、 模块定义 和 模块标识,规定了一个模块从引入到生产以及导出的整个过程。通过给 办法传递模块标识符(门路字符串或者模块名称)来引入 CJS 模块,导出时给 或者 赋值或者增加属性即可。
- Nodejs 引入模块的加载程序和根本步骤
1、加载程序和速度:
函数接管到模块标识符时,会优先查看内存中是否曾经有缓存的模块对象,有的话间接返回,没有就持续查找。所以缓存的加载优先级和加载速度是最高的,其次是外围模块,因为外围模块曾经被编译到了 Nodejs 代码中,Nodejs 启动的时候就曾经把外围模块的内容加载到了内存中,所以外围模块的加载程序和加载速度位于第二,仅次于内存。而后就是文件模块,Nodejs 通过找到文件而后应用对应的办法加载文件中的代码并执行。最初才是自定义模块。
2、加载根本步骤:
加载步骤大略有 、 和 三个过程。
Nodejs 在拿到模块标识符之后,会进行路径分析,取得了入口文件的绝对路径之后就会去内存检索,如果内存中没有缓存的话就会进入下一步,进行文件定位。留神自定义模块会有个 模块门路 的概念,加载自定义模块时会首先在以后文件的同级 目录下查找,如果没有找到的话就向上一级持续查找 ,直到零碎根目录(Windows 的盘符目录,比方 或者 *nix 的根目录 ),所以自定义模块的加载耗时最长。
路径分析之后会进行文件定位,尝试多种不同的扩展名而后判断文件是否存在,如果最终都不存在的话就会持续把这个模块当做自定义模块进行加载,如果还是找不到就间接报错。扩大判断的程序顺次为 、 和 。
- Nodejs 对于不同模块的编译形式
- JavaScript 模块通过包裹函数包裹之后交给零碎函数运行
- JSON 模块通过 转化为 JavaScript 对象而后返回后果
- C/C++ 模块通过零碎级的 函数加载执行
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/hd-nodejs/78135.html