小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言:
作为入门小白,Node的学习十分重要。前段时间看了下有关Node的知识,在这里做一个总结。
Node.js与Chrome V8
讲到Node不得不提的是V8引擎了。而Chrome浏览器也是V8引擎,他们在结构上都是基于事件驱动的异步架构。区别在于浏览器是通过事件驱动服务界面交互;Node是通过事件驱动服务I/O;
Node 的四大特性
- 异步I/O; 读取文件的耗时取决于最慢的文件读取耗时。
- 事件与回调函数(基于事件驱动);
- 单线程;
- 跨平台;
模块机制
首先了解一下JavaScript的变迁:
工具类库(浏览器兼容)---->组件库(功能模块)---->前端框架(功能模块组织)---->前端应用(业务模块组织)
我们发现JavaScript天生缺乏 模块。所以引出了CommonJS规范(希望Javascript可以在任何地方运行)
CommonJS的模块规范
主要分为:模块引用、模块定义、模块标识。
1. 模块引用
require()方法:接受模块标识,以此引入模块的API到当前上下文
var math = require('math')
2.模块定义
exports对象用于导出当前模块的方法或变量。
module对象代表模块自身
exports.add= function(){
//............
}
3.模块标识
其实就是传递给require()方法的参数,必须符合小驼峰命名的字符串,路径以 . 或者..的相对路径或绝对路径,可以没有后缀名。
Node的模块实现
node中引入模块的三个步骤
1.路径分析;2.文件定位3. 编译执行
Node中模块分为两类:
核心模块:Node提供的模块。
在node源码的编译过程中,编译进了二进制文件。在node进程启动时,核心模块被直接加载进内存,不需要文件定位和编译执行,在路径分析中优先判断,所以加载速度是最快的。
文件模块:用户编写的模块。
在运行时动态加载,需要完整的路径分析、文件定位、编译执行。速度比核心模块慢。
模块加载详细过程
1. 优先从缓存加载
和前端浏览器缓存静态脚本一样,node对引入过的模块会进行缓存。
不同:浏览器缓存文件,Node缓存编译和执行之后的对象
核心模块的缓存优先于文件模块缓存。
2. 路径分析和文件定位
2.1 模块标识符分析
模块标识符分类
- 核心模块:如http\fs\path\
- . / .. 开始的相对路径文件模块
- 以 / 开始的绝对路径文件模块
- 非路径形式的文件模块,如自定义的connect模块。放在node_modules目录下,查找最费时。
2.2 文件定位
-
文件扩展名分析
-
目录分析和包
Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件)----->通过JSON.pase()解析包描述对象 ----->从中取出main属性指定的文件名进行定位。
若main属性指定的文件名错误,或者没有package.json文件,Node会将index当作默认文件名,依次查找 index.js、index.node、index.json
2.3 模块编译
不同文件扩展名,载入方法也有所不同。
- .js 文件 -----fs模块同步读取后编译执行
- .node 文件 --------- 用C/C++编写的扩展名,通过dlopen()方法加载最后编译生成的文件
- .josn 文件 -----首先fs模块同步读取文件后,用JSON.parse()解析返回结果
- 其余扩展名,当作js文件载入
注意: 每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,提高二次引入的性能
1. Javascript模块的编译
如果直接把定义的模块的过程放在浏览器,会存在污染全局变量的情况。
事实上,Node对获取的JavaScript文件进行了包装。
(function (exports,require,module,__filename,__dirname){
//....
})
每个模块文件进行作用域隔离。
包装后通过 vm原生模块runInThisContext()方法执行,返回一个具体的function对象。最后,exports,require,module,__filename,__dirname作为参数传递给function执行
为什么存在exports的情况下,还存在module.exports?
exports对象是通过形参的方式传入的,直接赋值形参 会改变形参的引入。
如果要require引入一个类的效果,复制给module.exports 对象
2. C/C++模块的编译
Node调用process.dlopen()方法进行加载和执行。 .node 文件是编写C/C++模块之后编译生成的,不需要编译,只要加载和执行。所以执行效率高,但编写门槛比JavaScript高。
3. JSON文件的编译
1.fs模块读取JSON文件----》2. 调用JSON.parse()得到对象------》3. 赋给模块对象的export供外部调用
以上三种的文件模块的编译,即用户写的模块。
3. 核心模块
分为C/C++ 编写(放在Node的src目录)和JavaScript编写 (放在Node的lib目录)两部分,
3.1 JavaScript核心模块的编译过程
-
转存为 C/C++代码
V8 js2c.py工具 ,内置js代码---->转换成C++数组 -----》生成node_natives.h文件
-
编译JavaScript核心模块
获取源代码的方式从内存中加载
3.2 C/C++核心模块的编译过程
纯C/C++编写的部分统一称为内建模块,用户不直接调用。例如:Node中的buffer,crypto,evals,fs,os等模块都是部分通过C/C++编写的。
内建模块的优势:
- 由C/C++编写,性能优于脚本语言。
- 在文件编译时,内建模块会编译进二进制文件。Node执行时,会直接被加载进内存,不需要做标识符定位、文件定位、编译等,直接就可以执行。
依赖层级关系:
内建模块(C/C++)----->核心模块(JavaScript)------>文件模块
注:文件模块调用核心模块即可。核心模块基本都封装了内建模块
通常脚本语言的开发速度优于静态语言,但性能弱于静态语言,所以Node采用这种复合模式,找到了一个平衡点。
总结:
以上是我对Node的初认识和有关模块的知识。这只是Node的一小部分,今后还需继续学习,多总结,多成长。