资讯专栏INFORMATION COLUMN

深入浅出node.js总结-模块机制(1)

jifei / 704人阅读

摘要:先天就缺乏一项功能模块通过标签引入代码的方式显得杂乱无章,语言自身毫无组织和约束能力。与文件模块区别地方在于它从内存中加载缓存执行结果的位置核心模块在对象上,文件模块在对象上未完待续

javascript先天就缺乏一项功能:模块

javasciprt 通过script标签引入代码的方式显得杂乱无章,语言自身毫无组织和约束能力。人们不得不用命名空间等方式人为地约束代码,以求达到安全和易用的目的。

为了让javascript能在服务端有市场,社区为javascript制定了相应的规范——CommonJS

CommonJS规范 CommonJs的出发点

javascript有以下缺点:

==没有模块系统==

==标准库较少==-比如没有文件系统、I/O流等标准的API

==没有标准接口==-几乎没有定义过web服务器或者数据库之类的标准统一接口

==缺乏包管理系统==-javascript没有自动加载和安装依赖的能力

CommonJS就是来弥补这些缺陷的

CommonJS大部分规范依旧是草案,但是为javascript开发大型应用程序指明了一条道路

Node受到CommonJS的影响,CommonJS因Node表现优异而走入各个公司项目里,相互影响和促进

CommonJS的模块规范

分为模块引用、模块定义、模块标识

    ==模块引用== var math = require("math");

es6已更新一套支持模块引用的语法:import。在调用require时,可以把它放在某个判断条件下,但import不行;在打包编译时,如果require里的文件模块不存在,即便逻辑上不会进入其所在的判断条件,依旧会报错。可以使用try{}catch(e){}来处理

    ==模块定义==

提供了exports对象用于导出当前模块的方法或变量

export 是es6的规范,与import一同用,不要弄混了

一个模块里还有module对象,代表模块自身;exports是module的属性

在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性即可定义导出的方式

// math.js

exports.add = function(a,b){
    return a+b;
}

exports.sum = function(a,b){
    return a+b;
}
// index.js

var math = reqire("math");
exports.add = function(val){
    return math.add(val,1);
}

    ==模块标识==

指的是传给require()方法的参数,必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径

每个模块具有独立的空间,互不干扰,用户不必考虑变量污染

Node的模块实现

Node没有完全按照CommonJS规范实现,做了一定的取舍并加入自身需要的特性。

Node引入模块需经历如下3个步骤

    ==路径分析==

    ==文件定位==

    ==编译执行==

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

    核心模块在编译过程中,成为二进制执行文件;Node进程启动时,部分核心模块就直接加载进内存中

开发者引入这部分核心模块时,省略了文件定位、编译执行,且在路径分析中优先判断;故,加载速度最快

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

先路径分析文件定位、最后再运行,所以前面require一个不存在的文件时,即便有判断条件,依然会报错

优先从缓存加载

Node对引入过的模块都会进行缓存,以减少二次引入时的开销。==Node缓存的是编译和执行之后的对象==

所以,所有模块exports/export出的的东西,在内存中有且仅有一份,不受exports/export后的表达式/new之类的语句所影响

require()方法/import语句对于相同模块的二次加载都一律采用缓存优先的方式,这是第一优先级的。核心模块的缓存检查优于文件模块的缓存检查

路径分析和文件定位

因标识符的不同形式,模块的查找和定位有不同程度的差异

    模块标识符分析

标识符分为以下几类:

==核心模块==,如http、fs、path

.或..开始的==相对路径文件模块==

以/开始的==绝对路径文件模块==

==非路径形式的文件模块==

核心模块

优先级仅次于缓存加载,加载速度最快

不能将自定义模块的标识符与核心模块标识符定义得一致,除非你换用路径的方式

路径形式的文件模块

. .. /开始的标识符,均视为文件模块

require将路径转为真实路径,以此做索引,编译执行后的结果放入缓存

加载速度比核心模块慢

自定义模块

特殊的文件模块,查找费时,加载速度最慢

“模块路径”——Node定位文件模块时定制的查找策略。模块路径是一个路径组成的数组:

[
 当前文件目录下的xx目录,
 父目录下的xx目录,
 父目录下的父目录下的xx目录,
 沿路径向上逐级递归,直到根目录下的xx目录
]

类似于js的原型链或作用域链的查找方式。也正因如此,一旦路径越深,查找耗时就越多,所以加载速度最慢

    文件定位

有缓存的存在和前面的路径分析,文件定位相对比较简单。这里注意一些细节:

文件扩展名分析

有时标识符没有扩展名,CommonJS规范不允许不包含文件扩展名,但Node会按.js、.json、.node的顺序依次尝试

尝试时,会调用fs模块同步阻塞时判断文件是否存在。由于Node单线程,所以这里会引起性能问题——建议.node/.json文件加上扩展名

目录分析和包

有时没查找到文件,而是一个目录

Node会先找当前目录下的package.json,解析出main属性指定的文件名进行定位;如果文件名没有扩展名,则会进入扩展名分析的步骤

解析方式就是JSON.parse()

如果main提供的文件名有误,或者没有package.json,则查index(.js、.json、.json)

还没定位成功,则按照自定义模块的模块路径 策略,去父目录查询;如果模块路径数组遍历完毕都没找到,则抛出查找失败的异常

模块编译

(这里指的都是文件模块,非核心模块) 当定位到具体文件后,Node会新建一个模块对象,将文件载入并编译。载入方法根据不同文件扩展名而区分

.js文件:fs模块同步读取后编译执行

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

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

其余扩展名:当.js文件载入

编译成功的模块会缓存在Module._cache上,以文件路径作为索引

    javascript模块的编译

    编译过程中,Node对获取的javascript的文件内容进行了头尾包装,头部添加(function(==exports, require, module, __filename, __dirname==){ ,尾部添加 })

    __filename 完整的文件路径;__dirname 文件目录

    包装后的代码vm原生模块的runInThisContext()执行,返回一个具体的function对象

    最后,将当前模块对象的exports、require()、module、文件路径和目录作为参数传入给function对象

    执行后exports返回给调用方,其上的任何方法与属性均可被外部调用,但是模块中的其余变量或属性不可。==以此达到模块间的作用域隔离==

    这就是Node对CommonJS模块规范的实现

    exports的误用 编写代码时,理论上只要这样写就行:

    exports = function(){//My Class};
    

    但是这样写是有问题的,我们来看看编译过程:

      头尾包装:

    (function(exports, require, module, __filename, __dirname){
        exports = function(){//My Class};
    })
    

    看,你把形参改了。。。但是exports最后是要返回给调用方然后被外部调用的,==改形参根本不能真正改变exports对象的内容==

    所以,要不老老实实地写:exports.add = ...;或者写module.exports = ...

    c/c++模块的编译

    事实上,.node模块不需要编译,因为它是c/c++模块之后编译生成。它只需要加载和执行

    执行中exports对象与.node模块产生脸型,返回给调用者

    优势:执行效率高;劣势:编写门槛高

    JSON文件的编译

    fs模块同步读取JSON文件

    JSON.parse()方法得到对象

    赋给exports

    如果.json文件作为配置文件(如package.json),则不必调用fs模块去读取和解析,直接reqire()引入即可。同理它也是享受缓存的便利,二次引入时没有性能影响

核心模块

核心模块分为c/c++编写的和javascript编写的两部分

js核心模块的编译过程

编译程序会将js模块文件编译成c/c++代码

    转存为c/c++代码

    将所有内置的js代码转换成c++的数组,此时js代码不可直接执行,启动Node进程后,js代码直接加载进内存,将来查找比文件模块要快得多

    编译js核心模块

    经历头尾包装、执行、导出exports对象。与文件模块区别地方在于:它从内存中加载;缓存执行结果的位置

    核心模块在NativeModule._cache对象上,文件模块在Module._cache对象上

(未完待续~)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/7211.html

相关文章

  • Node.js内存管理和V8垃圾回收机制

    摘要:垃圾回收内存管理实践先通过一个来看看在中进行垃圾回收的过程是怎样的内存泄漏识别在环境里提供了方法用来查看当前进程内存使用情况,单位为字节中保存的进程占用的内存部分,包括代码本身栈堆。 showImg(https://segmentfault.com/img/remote/1460000019894672?w=640&h=426);作者 | 五月君Node.js 技术栈 | https:...

    JowayYoung 评论0 收藏0
  • Node - 异步IO和事件循环

    摘要:它是在的基础上改进的一种方案,通过对文件描述符上的事件状态进行判断。检索新的事件执行与相关的回调几乎所有情况下,除了关闭的回调函数,它们由计时器和排定的之外,其余情况将在此处阻塞。执行事件的,例如或者。 前言 学习Node就绕不开异步IO, 异步IO又与事件循环息息相关, 而关于这一块一直没有仔细去了解整理过, 刚好最近在做项目的时候, 有了一些思考就记录了下来, 希望能尽量将这一块的...

    MyFaith 评论0 收藏0
  • JavaScript - 收藏集 - 掘金

    摘要:插件开发前端掘金作者原文地址译者插件是为应用添加全局功能的一种强大而且简单的方式。提供了与使用掌控异步前端掘金教你使用在行代码内优雅的实现文件分片断点续传。 Vue.js 插件开发 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins译者:jeneser Vue.js插件是为应用添加全局功能的一种强大而且简单的方式。插....

    izhuhaodev 评论0 收藏0
  • node核心特性理解

    摘要:概述本文主要介绍了我对的一些核心特性的理解,包括架构特点机制核心模块与简单应用。在此期间,主线程继续执行其他任务。延续了浏览器端单线程,只用一个主线程执行,不断循环遍历事件队列,执行事件。 原文地址在我的博客,转载请注明来源,谢谢! node是在前端领域经常看到的词。node对于前端的重要性已经不言而喻,掌握node也是作为合格的前端工程师一项基本功了。知道node、知道后端的一些东西...

    huangjinnan 评论0 收藏0
  • 前端每周清单半年盘点之 Node.js

    摘要:前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。对该漏洞的综合评级为高危。目前,相关利用方式已经在互联网上公开,近期出现攻击尝试爆发的可能。 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热点、开发教程、工程实践、深度阅读、开源项目、巅峰人生等栏目。欢...

    kid143 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<