node浅析--模块

213 阅读4分钟

模块机制

接触过其他语言的带佬们可能知道,Java中有类文件,Python有import机制,Ruby有require,PHP有include和require。而早期JavaScript通过script引入代码的方式显得杂乱无章,语言自身毫无组织和约束能力,但是经过十多年的发展后,社区也为JS制定了相应的规范,其中CommonJS算是最为重要的里程碑

CommonJS 规范

总所周知,node的模块机制是基于CommonJS规范的,Node借鉴CommonJS的Modules规范实现了一套非常易用的模块系统,CommonJS对模块的定义十分简单,主要分为模块引用,模块定义和模块标识。

1.模块引用

var math = require('math')

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

2.模块定义及标识

在模块中,上下文提供require()方法引入外部模块,提供了exports对象用于导出,并且它是唯一导出的出口。模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者为相对路径和绝对路径,它可以没有文件名后缀js。

3.模块实现

综上所述,node的模块实现即为require的实现,在Node中引入模块,需要经过路径分析,文件定位,编译执行等三个步骤,在Node中,模块又分为Node本身提供的核心模块和用户编写的文件模块,核心部分的模块在Node源代码编译时就编译进了二进制执行文件,在Node进程启动时就直接写进了内存当中,并且在路径分析中优先判断,所以它的加载速度是最快的。

说到模块机制,面试的时候就常常会被问道这些问题,比如a.js 和 b.js 两个文件互相 require 是否会死循环? 双方是否能导出变量? 如何从设计上避免这种问题?还有比较 AMD, CMD, CommonJS 三者的区别等问题

如a.js 和 b.js 两个文件互相 require 是否会死循环? 双方是否能导出变量? 如何从设计上避免这种问题?

这是典型的循环引用的问题,如下所示的代码

 // a.js
exports.one = 'a1';
const b = require('./b');
console.log('b', b);

exports.two = 'a2';
exports.three = 'a3';

// b.js
const a = require('./a');
exports.oneInB = 'b1';

先执行的导出其未完成的副本, 通过导出工厂函数让对方从函数去拿比较好避免. 模块在导出的只是var module = { exports: {...} }; 中的 exports, 以从 a.js 启动为例, a.js 还没执行完会返回一个 a.jsexports 对象的 未完成的副本 给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。
那么我们在 require 一个模块(比如 lib)的时候,node 做了什么工作?为什么我们没有定义 require 方法,module 对象, firname 变量,dirname 变量但它们却能够在模块中使用呢?

模块编译

在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装,在头部添加了(function(exports, require, module, __filename, __direname){\n, ..../n});一个正常的JS文件会被包装成如下的样子:

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

包装之后的代码会通过VM原生模块的runInThisContext()方法执行(形如rval,只有明确的上下文,不污染全局)关于 VM 更多的一些接口可以先阅读官方文档VM (虚拟机)

AMD, CMD, CommonJS 三者的区别

AMD规范是CommonJS模块规范的一个延伸,使用如下方法进行定义

define(id?, dependencies?, factory)

AMD需要用define来明确定义一个模块,目的是进行作用域的隔离,防止那些全局变量及全局命名空间造成的变量污染,最后内容需要return进行返回,如下

define(function() {
    var exports = {}
    return exports
})

CMD与AMD的区别是AMD需要在声明模块的时候指定所有依赖,而CMD支持动态引入

    define(function(require, exports, module) {
        //Thoe module code goes here
    })

在需要依赖时,随时调用require()引入即可。