美文网首页一周内学会node.js
(二)NodeJs模块机制和异步I/O

(二)NodeJs模块机制和异步I/O

作者: 秋意思寒 | 来源:发表于2017-10-22 23:44 被阅读0次

    说明:该学习笔记参考《深入浅出Node.js》在学习过程中,添加了自己的理解和适当的补充!仅供参考!

    NodeJsmo模块机制

    CommonJs的模块规范

    模块引用

    示例代码如下:

    var math = require("math");
    

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

    模块定义

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

    //math.js
    
    exports.add = function(){
        var sum = 0,
        i = 0,
        args = arguments,
        l = args.length;
        while(i < 1){
            sum += args[i++];
        }
        return sum;
    }
    

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

    //program.js
    var math = require("./math");
    exports.iscrement = function(val){
        return math.add(val, 1);
    };
    

    这里,math.jsprogram.js在同一级目录下,最后在program.jsrequire("./math")

    模块标识

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

    每个模块具有独立的空间,它们相互不干扰,在引用时也显得干净利落。这套模块导出和引用机制使得用户完全不必考虑变量污染,命名空间等。

    NodeJs的模块实现

    在NodeJs中引入模块,需要经历以下三个步骤:

    • 1 路径分析
    • 2 文件定位
    • 3 编译执行

    模块分为两大类:

    • 核心模块,有NodeJs提供的模块。
    • 文件模块,有用户编写的模块。

    优先从缓存加载

    NodeJs对引入过的模块都会进行缓存,缓存的是编译和执行之后的对象。

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

    路径分析和文件定位

    模块标识符主要分为以下几类:

    • 核心模块,如httpfspath
    • .或者..开始的相对路径文件模块
    • / 开始的绝对路径文件模块
    • 非路径形式的文件模块,如自定义的connect模块自定义模块

    如果试图加载一个与核心模块标识符相同的,那是不会成功的。想要加载成功,必须选择一个不同的标识符或者换用路径的方式。

    自定义模块的查找是最费时间的。

    模块路径是NodeJs在定义文件模块的具体文件时定制的查找策略,具体表现为一个路径组成的数组。它的生成方式与JavaScript的原型链或作用域链的查找方式十分类似。在加载的的过程中,NodeJs会逐个尝试模块路径中的路径,直到找到目标文件为止。

    // moudle_path.js
    console.log(module.paths);
    

    文件定位
    require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJs模块规范也允许标识符中不包含文件扩展名,这种情况下,NodeJs会按.js.json.node的次序补足扩展名,依次尝试。

    文件定位流程图

    在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在,由于NodeJs是单线程的,所以这里会引起一个性能问题。

    如果是.node.json文件,在传递给require()的标识符中带上扩展名,会加快一些速度。
    同步配合缓存,可以大幅度缓解NodeJs单线程中阻塞式调用的缺陷。

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

    模块编译

    在NodeJs中,每个文件都是一个对象,它的定义如下:

    function Module(id, parent){
        this.id = id;
        this.exports = {};
        this.parent = parent;
        if (parent && parent.children){
            parent.children.push(this);
        }
        
        this.filename = null;
        this.loaded = false;
        this.children = [];
    }
    

    定位到文件之后,NodeJs会新建一个模块对象,然后根据路径载入并编译。不同的文件拓展名,其载入的方法有所不同。

    • .js文件,通过fs模块同步读取文件后编译执行。
    • .node,这是C/C++编写的拓展文件,通过dlopen()方法加载最后编译生成的文件。
    • .json,通过fs模块同步读取文件后,用JSON.parse()解析后返回结果。
    • 其余扩展名文件,当作.js文件载入。

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

    JavaScript模块的编译

    每个模块文件中存在着 requireexportsmodule 这3个变量,在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了 (function (exports, require, module, __filename, __dirname) {\n ,在尾部添加了\n}); 。一个正常的JavaScript文件会被包装成如下的样子:

    (function (exports, require, module, __filename, __dirname) {
        var math = require('math');
        exports.area = function (radius) {
          return Math.PI * radius * radius;
        };
    });
    

    这 样 每 个 模 块 文 件 之 间 都 进 行 了 作 用 域 隔 离 。 包 装 之 后 的 代 码 会 通 过 vm 原 生 模 块 的runInThisContext () 方法执行(类似 eval ,只是具有明确上下文,不污染全局),返回一个具体的function 对象。

    exports 对象是通过形参的方式传入的,直接赋值形参会改变形参的引用,但并不能改变作用域外的值。

    var change = function (a){
      a = 100;
      console.log(a);  // 100
    }
    var a = 10;
    change(a);
    console.log(a);  // 10
    

    C/C++模块的编译

    JSON文件的编译

    NodeJs利用 fs 模块同步读取JSON文件的内容之后,调用 JSON.parse () 方法得到对象,然后将它赋给模块对象的 exports ,以供外部调用。

    NodeJs核心模块

    NodeJs的核心模块在编译成可执行文件的过程中被编译进了二进制文件。核心模块其实分为C/C++编写的和JavaScript编写的两部分,其中C/C++文件存放在Node项目的src目录下,JavaScript文件存放在lib目录下。

    NodeJs的 buffercryptoevalsfsos 等模块都是部分通过C/C++编写的。

    依赖层次关系

    JavaScript的一个典型弱点就是位运算。JavaScript的位运算参照Java的位运算实现,但是Java位运算是在 int 型数字的基础上进行的,而JavaScript中只有 double 型的数据类型,在进行位运算的过程中,需要将 double 型转换为 int 型,然后再进行。所以,在JavaScript层面上做位运算的效率不高。

    包与 NPM

    CommonJS的包规范的定义其实也十分简单,它由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者则用于描述包的相关信息,以供外部读取分析。

    包组织模块示意图

    包实际上是一个存档文件,即一个目录直接打包为.zip或tar.gz格式的文件,安装后解压还原为目录。完全符合CommonJS规范的包目录应该包含如下这些文件。

    • package.json:包描述文件。
    • bin:用于存放可执行二进制文件的目录。
    • lib:用于存放JavaScript代码的目录。
    • doc:用于存放文档的目录。
    • test:用于存放单元测试用例的代码。

    包描述文件用于表达非代码相关的信息,它是一个JSON格式的文件——package.json,位于包的根目录下,是包的重要组成部分。而NPM的所有行为都与包描述文件的字段息息相关。

    对于NodeJs而言,NPM帮助完成了第三方模块的发布、安装和依赖等。借助NPM,Node与第三方模块之间形成了很好的一个生态系统。借助NPM,可以帮助用户快速安装和管理依赖包。

    NPM常用功能

    查看帮助

    • 在安装Node之后,执行 npm –v 命令可以查看当前NPM的版本
    npm -v
    
    • 在不熟悉NPM的命令之前,可以直接执行NPM查看到帮助引导说明
    npm
    
    • 安装依赖包是NPM最常见的用法,它的执行语句是
    npm install express
    

    NPM会在当前目录下创建node_modules目录,然后在node_modules目录下创建express目录,接着将包解压到这个目录下。

    • 进行全局模式安装
    npm install express –g
    

    需要注意的是,全局模式并不是将一个模块包安装为一个全局包的意思,它并不意味着可以从任何地方通过 require() 来引用到它。实际上, -g 是将一个包安装为全局可用的可执行命令。它根据包描述文件中的 bin 字段配置,将实际脚本链接到与NodeJs可执行文件相同的路径下。

    • 如果不能通过官方源安装,可以通过镜像源安装,在执行命令时,添加 --registry=http://registry.url即可
    npm install underscore --registry=http://registry.url
    

    如果使用过程中几乎都采用镜像源安装,可以执行以下命令指定默认源

    npm config set registry http://registry.url
    

    在执行 npm uninstall <package>时, uninstall 指向的脚本也许会
    做一些清理工作等。

    • 分析出当前路径下能够通过模块路径找到的所有包,并生成依赖树
    npm ls
    

    异步I/O

    相关文章

      网友评论

        本文标题:(二)NodeJs模块机制和异步I/O

        本文链接:https://www.haomeiwen.com/subject/hpfouxtx.html