位置: 编程技术 - 正文

深入理解Commonjs规范及Node模块实现(深入理解计算机系统)

编辑:rootadmin

推荐整理分享深入理解Commonjs规范及Node模块实现(深入理解计算机系统),希望有所帮助,仅作参考,欢迎阅读内容。

文章相关热门搜索词:深入理解计算机系统,深入理解计算机系统,深入理解新发展理念,推进供给侧结构性改革 的题目,深入理解计算机系统,深入理解中国式现代化,深入理解新发展理念,深入理解新发展理念,推进供给侧结构性改革,深入理解新发展理念,推进供给侧结构性改革,内容如对您有帮助,希望把文章链接给更多的朋友!

前面的话

Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。本文将详细介绍NodeJS的模块实现

引入

nodejs是区别于javascript的,在javascript中的顶层对象是window,而在node中的顶层对象是global

[注意]实际上,javascript也存在global对象,只是其并不对外访问,而使用window对象指向global对象而已

在javascript中,通过var a = ;是可以通过window.a来得到的

但在nodejs中,是不能通过global.a来访问,得到的是undefined

这是因为var a = ;这个语句中的变量a,只是模块范围内的变量a,而不是global对象下的a

在nodejs中,一个文件就是一个模块,每个模块都有自己的作用域。使用var来声明的一个变量,它并不是全局的,而是属于当前模块下

如果要在全局作用域下声明变量,则如下所示

概述

Node中模块分为两类:一类是Node提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块

核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的

文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢

接下来,我们展开详细的模块加载过程

模块加载

在javascript中,加载模块使用script标签即可,而在nodejs中,如何在一个模块中,加载另一个模块呢?

使用require()方法来引入

【缓存加载】

再展开介绍require()方法的标识符分析之前,需要知道,与前端浏览器会缓存静态脚本文件以提高性能一样,Node对引入过的模块都会进行缓存,以减少二次引入时的开销。不同的地方在于,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象

不论是核心模块还是文件模块,require()方法对相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。不同之处在于核心模块的缓存检查先于文件模块的缓存检查

【标识符分析】

require()方法接受一个标识符作为参数。在Node实现中,正是基于这样一个标识符进行模块查找的。模块标识符在Node中主要分为以下几类:[1]核心模块,如http、fs、path等;[2].或..开始的相对路径文件模块;[3]以/开始的绝对路径文件模块;[4]非路径形式的文件模块,如自定义的connect模块

根据参数的不同格式,require命令去不同路径寻找模块文件

1、如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js

2、如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js

3、如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)

[注意]如果是当前路径下的文件模块,一定要以./开头,否则nodejs会试图去加载核心模块,或node_modules内的模块

【文件扩展名分析】

require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node会先查找是否存在没有后缀的该文件,如果没有,再按.js、.json、.node的次序补足扩展名,依次尝试

在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是.node和.json文件,在传递给require()的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解Node单线程中阻塞式调用的缺陷

【目录分析和包】

在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时经常会出现,此时Node会将目录当做一个包来处理

在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤

而如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node

如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常

访问变量

如何在一个模块中访问另外一个模块中定义的变量呢?

【global】

深入理解Commonjs规范及Node模块实现(深入理解计算机系统)

最容易想到的方法,把一个模块定义的变量复制到全局环境global中,然后另一个模块访问全局环境即可

这种方法虽然简单,但由于会污染全局环境,不推荐使用

【module】

而常用的方法是使用nodejs提供的模块对象Module,该对象保存了当前模块相关的一些信息

module.id 模块的识别符,通常是带有绝对路径的模块文件名。 module.filename 模块的文件名,带有绝对路径。 module.loaded 返回一个布尔值,表示模块是否已经完成加载。 module.parent 返回一个对象,表示调用该模块的模块。 module.children 返回一个数组,表示该模块要用到的其他模块。 module.exports 表示模块对外输出的值。

【exports】

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量

为了方便,Node为每个模块提供一个exports变量,指向module.exports。造成的结果是,在对外输出模块接口时,可以向exports对象添加方法

[注意]不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系

模块编译

编译和执行是模块实现的最后一个阶段。定位到具体的文件后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也有所不同,具体如下所示

js文件——通过fs模块同步读取文件后编译执行

node文件——这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件

json文件——通过fs模块同步读取文件后,用JSON.parse()解析返回结果

其余扩展名文件——它们都被当做.js文件载入

每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能

根据不同的文件扩展名,Node会调用不同的读取方式,如.json文件的调用如下:

其中,Module._extensions会被赋值给require()的extensions属性,所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式。编写如下代码测试一下:

得到的执行结果如下:

在确定文件的扩展名之后,Node将调用具体的编译方式来将文件执行后返回给调用者

【JavaScript模块的编译】

回到CommonJS模块规范,我们知道每个模块文件中存在着require、exports、module这3个变量,但是它们在模块文件中并没有定义,那么从何而来呢?甚至在Node的API文档中,我们知道每个模块中还有filename、dirname这两个变量的存在,它们又是从何而来的呢?如果我们把直接定义模块的过程放诸在浏览器端,会存在污染全局变量的情况

事实上,在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了(function(exports, require, module, filename, dirname) {n,在尾部添加了n});

一个正常的JavaScript文件会被包装成如下的样子

这样每个模块文件之间都进行了作用域隔离。包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行

这就是这些变量并没有定义在每个模块文件中却存在的原因。在执行之后,模块的exports属性被返回给了调用方。exports属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用

至此,require、exports、module的流程已经完整,这就是Node对CommonJS模块规范的实现

【C/C++模块的编译】

Node调用process.dlopen()方法进行加载和执行。在Node的架构下,dlopen()方法在Windows和*nix平台下分别有不同的实现,通过libuv兼容层进行了封装

实际上,.node的模块文件并不需要编译,因为它是编写C/C++模块之后编译生成的,所以这里只有加载和执行的过程。在执行的过程中,模块的exports对象与.node模块产生联系,然后返回给调用者

C/C++模块给Node使用者带来的优势主要是执行效率方面的,劣势则是C/C++模块的编写门槛比JavaScript高

【JSON文件的编译】

.json文件的编译是3种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports,以供外部调用

JSON文件在用作项目的配置文件时比较有用。如果你定义了一个JSON文件作为配置,那就不必调用fs模块去异步读取和解析,直接调用require()引入即可。此外,你还可以享受到模块缓存的便利,并且二次引入时也没有性能影响

CommonJS

在介绍完Node的模块实现之后,回到头来再学习下CommonJS规范,相对容易理解

CommonJS规范的提出,主要是为了弥补当前javascript没有标准的缺陷,使其具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段

CommonJS对模块的定义十分简单,主要分为模块引用、模块定义和模块标识3个部分

【模块引用】

在CommonJS规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中

【模块定义】

在模块中,上下文提供require()方法来引入外部模块。对应引入的功能,上下文提供了exports对象用于导出当前模块的方法或者变量,并且它是唯一导出的出口。在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式:

在另一个文件中,我们通过require()方法引入模块后,就能调用定义的属性或方法了

【模块标识】

模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js

模块的定义十分简单,接口也十分简洁。它的意义在于将类聚的方法和变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅地连接上下游依赖。每个模块具有独立的空间,它们互不干扰,在引用时也显得干净利落

标签: 深入理解计算机系统

本文链接地址:https://www.jiuchutong.com/biancheng/380788.html 转载请保留说明!

上一篇:websocket+node.js实现实时聊天系统问题咨询(websocket npm)

下一篇:Nodejs中使用captchapng模块生成图片验证码(nodejs cgi)

  • 一般纳税人资格证明
  • 金蝶软件可以实现一键报税吗
  • 金税四期上线后还需要会计吗
  • 企业筹建期间契税账务处理
  • 差额征税扣除额是什么意思
  • 社保免征还需要申报吗
  • 小微企业增值税优惠政策最新2023
  • 哪些支出允许在企业所得税前扣除
  • 技术转让所得的税收优惠
  • 销售土地如何交增值税
  • 结转本月工资薪金
  • 施工单位名称变更需要变施工许可证吗
  • 运输费计入采购成本的分录
  • 无形资产摊销的年限规定
  • 公司注销库存商品转营业外收入
  • 员工工资超过多少钱要交税
  • 增值税税率和征收率有什么区别
  • 关于免租金的法律规定
  • 卖仪表赚钱吗
  • 无形资产怎么核算成本
  • 每年空调的维保费用怎么入账?
  • 纳税人证明怎么办理
  • 银行流水账单怎么查
  • 以前年度损益调整属于哪类科目
  • 不动产融资租赁服务属于租赁服务吗
  • 跨年度冲减收入
  • 高新技术企业人员工资占比
  • deepin启动延时
  • macbookpro怎么安装双系统
  • 增值税出口退税为什么不属于政府补助
  • 本月印花税下月缴纳需要计提吗
  • ati2mdxx.exe是什么进程 ati2mdxx进程信息查询
  • php 字符串
  • nerosmartstart.exe - nerosmartstart是什么进程 作用是什么
  • 房产增值税计算公式2022最新
  • 固定资产全额折旧
  • 补发工资怎么做账
  • 实例讲解YII2中多表关联的使用方法
  • 查看redis节点
  • 以摊余成本计量和以公允价值计量的区别
  • 差额征税怎么交税
  • CNN卷积神经网络/手写数字识别[VHDL][MATLAB]带源码
  • 3分钟认识Vue3的v-model
  • 暂估入库以及暂估的区别
  • 劳务外经证预缴税款
  • 专用发票超过360天未认证
  • 土地增值税的扣除项目金额有哪些
  • 个税出现负数是什么意思
  • 无形资产摊销完以后得账务处理
  • 车辆处置收入怎么交税
  • 固定资产的原价减去预计净残值等于什么
  • 委托收款背书和质押背书都属于转让背书
  • 700元打8折怎么算
  • 注册资金没显示
  • 行政单位的财务报表包括哪些
  • 咨询费的支出范围
  • 酒店财务帐务处理方案
  • 数据库备份怎么做mysql
  • win7怎么下载win10
  • Windows Server 2008的NPS策略应用
  • win8.1iso
  • 系统运行缓慢解决方法
  • windows 7光盘
  • win7系统防火墙有必要开吗
  • win7任务管理器怎么打开
  • jQuery实现checkbox即点即改批量删除及中间遇到的坑
  • android通知消息
  • python接入微信
  • node.js ssh
  • css渐变文本效果在哪
  • python编写飞机大战
  • jquery常见问题
  • android内存优化三 简书
  • 在linux安装python
  • 开票物品名称要求
  • 如何知道公司所有账户
  • 营改增后一般纳税人动产租赁税率
  • 江苏国税申报
  • 天津税务陈岩
  • 武汉市第二税务稽查局地址
  • 免责声明:网站部分图片文字素材来源于网络,如有侵权,请及时告知,我们会第一时间删除,谢谢! 邮箱:opceo@qq.com

    鄂ICP备2023003026号

    网站地图: 企业信息 工商信息 财税知识 网络常识 编程技术

    友情链接: 武汉网站建设