前言
本文主要是希望对node的知识点做一个整理,之前学习的比较散,难免会有疏漏,希望可以通过文章进行一个系统的梳理和学习,同时也会将一些面试题整理进来加强学习。关于node的基本介绍就不多赘述,直接从知识点开始,本文是系列的第一篇——模块机制。
模块规范
Commonjs
在 es6规范 中的 import 普及之前,JavaScript有一个先天不足的地方就是缺少模块系统,而早期CommonJs规范对Node的影响极大,同时Node的火爆也进一步促进了CommonJs的发展。
Node应用采用模块组成,使用CommonJs的模块规范,CommonJs模块规范特点如下:
- 每个文件就是一个模块,有独立作用域,运行在自己的作用域,不会污染全局作用域
- 模块第一次加载后会缓存,以后再加载会默认读取缓存结果,需要重新加载必须清除缓存
- 模块加载的顺序,按照其在代码中出现的顺序。
CoomonJs模块规范包括三个核心部分:
- 模块引用
var Math = require('math');
- 模块定义
// math.js
function math () {
...
}
module.exports = math
- 模块标识
模块标识就是require() 括号内的参数
module对象
在 Node 中每个文件都是一个模块,每个模块都是有 Node 提供的 Module构建函数 生成的对象实例。
例子:
var math = require('fs');
console.log(module);
// 返回结果
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/jeffrey/Desktop/mySpace/debugStudy/app.js',
loaded: false,
children: [],
paths:
[ '/Users/jeffrey/Desktop/mySpace/debugStudy/node_modules',
'/Users/jeffrey/Desktop/mySpace/node_modules',
'/Users/jeffrey/Desktop/node_modules',
'/Users/jeffrey/node_modules',
'/Users/node_modules',
'/node_modules' ]
}
- module.id 模块的识别符,通常是带有绝对路径的模块文件名。
- module.filename 模块的文件名,带有绝对路径。
- module.loaded 返回一个布尔值,表示模块是否已经完成加载。
- module.parent 返回一个对象,表示调用该模块的模块。
- module.children 返回一个数组,表示该模块要用到的其他模块。
- module.exports 表示模块对外输出的值。
我们经常会遇到的一个问题:exports 和 module.exports 的区别(面试题),记住并理解以下几点即可
- 初始状态下,模块的全局变量 exports 是 module.exports 的同一个引用
- require 引用模块后,返回的是 module.exports
- exports.something = something 相当于在module.exports 上面直接挂属性
- exports = something 相当于给exports重新赋值,也就断了和module.exports的联系,
- module.exports = something 相当于给module.exports 重新赋值,也与exports断了联系
模块分类
核心模块
Node程序自身提供的模块
在node源代码编译的过程中,就编译进了二进制执行文件,属于安装包的一部分。在node进程启动时,部分核心模块会被直接加载进内存,因此,这部分核心模块的引入不需要文件定位和编译执行,并且优先进行路径分析,所以核心模块加载速度最快。如果想要提高自己的node的加载速度,可以把自己的包,写入到安装包装中,使之变成核心模块。
文件模块
用户自己编写的模块和网络上的第三方模块 文件模块是在运行时动态加载的,需要完整的路径分析、文件定位、编译执行的过程,加载速度比核心模块加载的速度要慢。
加载策略
Node中对模块进行加载主要经过 路径分析 --> 文件定位 --> 载入执行 的过程
路径分析
之前提到的CommonJs规范中模块标识可以有多种形式
- 核心模块:如http、fs、path等。
- .或者/开始的相对路径文件模块。
- 以/开始的绝对路径文件模块。
- 非路径形式的文件模块
Node会根据以上几种不同的情况,制定不同的加载策略
文件定位
- 核心模块
它在node的源代码编辑过程中已经编译为二进制代码,如果模块不以 . 或者/ 为开头如果发现有该核心模块会直接执行核心模块。 - .或者/开头
表示加载的是一个位于绝对路径或者相对路径的模块文件,会按照路径进行查找同时Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。 - 文件模块
通常这种情况是目录的加载,require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。
整理过程如下:
(图片转载自www.cnblogs.com/misscai/p/1…
缓存
获取到模块地址后,Node就开始载入模块,载入后会将模块缓存起来,如果再次加载该模块,Node会优先判断模块是否存在缓存中,如果再缓存中就直接返回缓存的结果,所以如果想重新加载需要删除缓存
// 删除指定模块的缓存
delete require.cache[moduleName];
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key){deleterequire.cache[key];})
循环依赖
经常会遇到a.js 依赖 b.js 而 b.js 依赖 a.js 的问题(面试题)
// a.js
var b = require('./b'); // step1
console.log('b:' + b.name); // step5
exports.name = 'a'; // step6
// b.js
var a = require('./a'); // step2
console.log('a:' + a.name); // step3
exports.name = 'b'; // step4
node a
// 返回结果
a:undefined
b:b
这段代码的执行顺序是按照上面注释的 step1-6来执行的,当node a 执行的时候,node就给a创建了一个module对象存储到了cache中,执行到step1时 开始加载 b.js,执行到 step2 时 读取了a存储在cache中的对象,但是此时a还没有执行到exports.name = 'a' 所以导出去的是一个空对象也就没有值。
所以没有陷入死循环的根本原因是:node加载模块时一开始就创建了module对象并赋予到内存中,然后再通过执行exports操作或者module.exports操作 给 module.exports赋值。